Showing preview only (1,334K chars total). Download the full file or copy to clipboard to get everything.
Repository: davidstump/SwiftPhoenixClient
Branch: master
Commit: 25a4d8a3fa75
Files: 116
Total size: 1.2 MB
Directory structure:
gitextract_igy9h_6n/
├── .gitignore
├── .slather.yml
├── .sourcery.yml
├── .travis.yml
├── CHANGELOG.md
├── Cartfile.private
├── Cartfile.resolved
├── Examples/
│ └── Basic/
│ ├── AppDelegate.swift
│ ├── Assets.xcassets/
│ │ ├── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── Base.lproj/
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── Info.plist
│ ├── SceneDelegate.swift
│ ├── basic/
│ │ └── BasicChatViewController.swift
│ └── chatroom/
│ └── ChatRoomViewController.swift
├── Gemfile
├── LICENSE
├── Package.swift
├── README.md
├── RELEASING.md
├── Sources/
│ ├── Supporting Files/
│ │ ├── Info.plist
│ │ └── SwiftPhoenixClient.h
│ └── SwiftPhoenixClient/
│ ├── Channel.swift
│ ├── Defaults.swift
│ ├── Delegated.swift
│ ├── HeartbeatTimer.swift
│ ├── Message.swift
│ ├── PhoenixTransport.swift
│ ├── Presence.swift
│ ├── Push.swift
│ ├── Socket.swift
│ ├── SynchronizedArray.swift
│ └── TimeoutTimer.swift
├── SwiftPhoenixClient.podspec
├── SwiftPhoenixClient.xcodeproj/
│ ├── project.pbxproj
│ ├── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata/
│ └── xcschemes/
│ ├── Basic.xcscheme
│ ├── RxSwiftPhoenixClient.xcscheme
│ ├── StarscreamSwiftPhoenixClient.xcscheme
│ ├── SwiftPhoenixClient.xcscheme
│ └── SwiftPhoenixClientTests.xcscheme
├── Tests/
│ ├── Fakes/
│ │ ├── FakeTimerQueue.swift
│ │ ├── FakeTimerQueueSpec.swift
│ │ └── SocketSpy.swift
│ ├── Helpers/
│ │ └── TestHelpers.swift
│ ├── Info.plist
│ ├── Mocks/
│ │ ├── MockableClass.generated.swift
│ │ └── MockableProtocol.generated.swift
│ └── SwiftPhoenixClientTests/
│ ├── ChannelSpec.swift
│ ├── DefaultSerializerSpec.swift
│ ├── HeartbeatTimerSpec.swift
│ ├── MessageSpec.swift
│ ├── PresenceSpec.swift
│ ├── SocketSpec.swift
│ ├── TimeoutTimerSpec.swift
│ └── URLSessionTransportSpec.swift
├── docs/
│ ├── Classes/
│ │ ├── Channel.html
│ │ ├── Defaults.html
│ │ ├── Message.html
│ │ ├── Presence/
│ │ │ ├── Events.html
│ │ │ └── Options.html
│ │ ├── Presence.html
│ │ ├── Push.html
│ │ └── Socket.html
│ ├── Classes.html
│ ├── Enums/
│ │ └── ChannelState.html
│ ├── Enums.html
│ ├── Global Variables.html
│ ├── Protocols/
│ │ └── Serializer.html
│ ├── Protocols.html
│ ├── Structs/
│ │ ├── ChannelEvent.html
│ │ └── Delegated.html
│ ├── Structs.html
│ ├── Typealiases.html
│ ├── css/
│ │ ├── highlight.css
│ │ └── jazzy.css
│ ├── docsets/
│ │ ├── SwiftPhoenixClient.docset/
│ │ │ └── Contents/
│ │ │ ├── Info.plist
│ │ │ └── Resources/
│ │ │ ├── Documents/
│ │ │ │ ├── Classes/
│ │ │ │ │ ├── Channel.html
│ │ │ │ │ ├── Defaults.html
│ │ │ │ │ ├── Message.html
│ │ │ │ │ ├── Presence/
│ │ │ │ │ │ ├── Events.html
│ │ │ │ │ │ └── Options.html
│ │ │ │ │ ├── Presence.html
│ │ │ │ │ ├── Push.html
│ │ │ │ │ └── Socket.html
│ │ │ │ ├── Classes.html
│ │ │ │ ├── Enums/
│ │ │ │ │ └── ChannelState.html
│ │ │ │ ├── Enums.html
│ │ │ │ ├── Global Variables.html
│ │ │ │ ├── Protocols/
│ │ │ │ │ └── Serializer.html
│ │ │ │ ├── Protocols.html
│ │ │ │ ├── Structs/
│ │ │ │ │ ├── ChannelEvent.html
│ │ │ │ │ └── Delegated.html
│ │ │ │ ├── Structs.html
│ │ │ │ ├── Typealiases.html
│ │ │ │ ├── css/
│ │ │ │ │ ├── highlight.css
│ │ │ │ │ └── jazzy.css
│ │ │ │ ├── index.html
│ │ │ │ ├── js/
│ │ │ │ │ └── jazzy.js
│ │ │ │ ├── search.json
│ │ │ │ └── undocumented.json
│ │ │ └── docSet.dsidx
│ │ └── SwiftPhoenixClient.tgz
│ ├── index.html
│ ├── js/
│ │ └── jazzy.js
│ ├── search.json
│ └── undocumented.json
├── fastlane/
│ ├── Appfile
│ ├── Fastfile
│ └── README.md
└── sourcery/
├── MockableClass.stencil
├── MockableProtocol.stencil
└── MockableWebSocketClient.stencil
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# OS X
.DS_Store
# Xcode
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
profile
*.moved-aside
DerivedData
*.hmap
*.xccheckout
# AppCode
.idea/
Carthage
Demo/Pods
.ruby-version
.ruby-gemset
# Swift Package Manager
.build
.swiftpm
Packages
Package.pins
================================================
FILE: .slather.yml
================================================
# .slather.yml
# CodeCov
coverage_service: cobertura_xml
xcodeproj: SwiftPhoenixClient.xcodeproj
scheme: SwiftPhoenixClient
workspace: SwiftPhoenixClient.xcworkspace
source_directory: SwiftPhoenixClient
output_directory: fastlane/test_output
ignore:
- ../*
- Pods/*
================================================
FILE: .sourcery.yml
================================================
sources:
- Sources/
templates:
- sourcery/
output:
Tests/Mocks/
================================================
FILE: .travis.yml
================================================
language: swift
os: osx
osx_image: xcode11.4
cache:
bundler: true
directories:
- Carthage
before_install:
- brew update
- brew outdated carthage || brew upgrade carthage
- carthage bootstrap --use-xcframeworks --platform iOS --no-use-binaries --cache-builds
script:
- bundle exec fastlane test
# after_success:
# - bash <(curl -s https://codecov.io/bash)
================================================
FILE: CHANGELOG.md
================================================
# CHANGELOG
All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/)
This product uses [Semantic Versioning](https://semver.org/).
### 5.3.5
- Fix `objc_loadWeakRetained` on iOS 18 in the `Transport` layer
### 5.3.4
- Added various weak self checks
### 5.3.3
- Added weak self references in heartbeat timer
- Added guards against weak self in PhoenixTransport
### 5.3.3
- Additional thread related crashes
### 5.3.2
- Fixed various thread-related crashes
### 5.3.1
- Added `socket.headers` which will be added to the `URLRequest` when opening a WebSocket connection
- Using thread-safe array for Socket callback bindings, fixing a crash when creating a channel
- Breaking a retain cycle in socket
### 5.3.0
- Fix retain cycles in `URLSessionTransport` and using default operation queue
- Adding an optional `leeway` to the `HeartbeatTimer`
- Added additional `open` methods in `URLSessionTransport` for further customization
- Using a thread-safe array for Channel bindings
## 5.2.2
- Changed `URLSessionTransport` to `open` to provide for custom behavior, such as SSL Pinning
## 5.2.1
- Added `connectionState` to `Socket` which exposes the Socket's ready state
## 5.2.0
- [#226](https://github.com/davidstump/SwiftPhoenixClient/pull/226) Adds `URLResponse` as an optional value in `socket.onError` callbacks to allow for checking status codes from the server when the Socket connection errors out. See Examples in PR for more details
## 5.1.0
- Improves reconnection logic around a heartbeat timeout
## 5.0.0
- Removes RxSwift dependency
- Removes Starscream dependency
- Creating new repos to host these extensions
## 4.0.0
- Updates RxSwift version to 6.x
## 3.0.0
This ia a **BREAKING** release. The following has changed to properly matched the phoenix.js library
- `message.payload.response` is now automatically unwrapped and returned as `message.payload` for `phx_reply` events.
- The client now, be default, uses the JSON V2 Serializer which was added in phoenix 1.3. If you are still running 1.2 or earlier, then you will need to
continue using SwiftPhoenixClient 2.1.0, or provide your own custom `vsn`, `encoder` and `decoder` to the `Socket` class.
## 2.1.1
- Fixed HeartbeatTimer to add thread safety and fix crash reported in #188
## 2.1.0
- Updated Presence.Options init method to be public
- Updated URLSessionWebsocketTask init method to accept a custom configuration
## 2.0.0
- Restructured project
- Added support for URLSession's Websocket Task
- Split Starscream and RxSwift into optional modules
## [1.3.0]
- Fixed Cartfile declaration of Starscream
- Added `HeartbeatTimer` class which allows running Timers to run on their own thread
- Made `Socket` init public to allow customization of the transport methhod
## [1.2.1]
- Pinned back Starscream version to fix Carthage build issue
## [1.2.0](https://github.com/davidstump/SwiftPhoenixClient/compare/1.1.2...1.2.0)
- [#153](https://github.com/davidstump/SwiftPhoenixClient/pull/153): Added ability to pass a closure when initializing a `Socket` to dynamically change `params` when reconnecting
- Fixed Package.swift and updated it to use latest Starscream
## [1.1.2](https://github.com/davidstump/SwiftPhoenixClient/compare/1.1.1...1.1.2)
- [#151](https://github.com/davidstump/SwiftPhoenixClient/pull/151): Made isJoined, isJoining, etc methods on Channel public
## [1.1.1](https://github.com/davidstump/SwiftPhoenixClient/compare/1.1.0...1.1.1)
- [#141](https://github.com/davidstump/SwiftPhoenixClient/pull/141): tvOS support
- [#145](https://github.com/davidstump/SwiftPhoenixClient/pull/145): Refactored Socket reconnect strategy
- [#146](https://github.com/davidstump/SwiftPhoenixClient/pull/146): Refactored Channel rejoin strategy
## [1.1.0]
- Swift 5
## [1.0.1]
- Fixed issue with Carthage installs
## [1.0.0]
- Rewrite of large parts of the Socket and Channel classes
- Optional API for automatic retain cycle handling
- Presence support
## [0.9.3]
## Added
- [#119](https://github.com/davidstump/SwiftPhoenixClient/pull/119): A working implementation of Presence
## Changed
- [#120](https://github.com/davidstump/SwiftPhoenixClient/pull/120): Xcode 10 and Swift 4.2
## [0.9.2]
## Fixed
- [#111](https://github.com/davidstump/SwiftPhoenixClient/pull/111): Strong memory cycles between Socket, Channel and Timers
- [#112](https://github.com/davidstump/SwiftPhoenixClient/pull/112): Leak when Socket disconnects and properly call `onClose()`
- [#114](https://github.com/davidstump/SwiftPhoenixClient/pull/114): Carthage failing on builds and app store uploads
## Changed
- [#116](https://github.com/davidstump/SwiftPhoenixClient/pull/116): A Channel's `topic` is now exposed as `public`
## [0.9.1]
### Added
- Added security configuration to the underlying WebSocket.
## [0.9.0]
Continue to improve the API and behavior of the library to behave similar to the JS library. This release introduces
some breaking changes in the API that will require updates to your code. See the [usage guide] for help.
### Updated
- Swift 4.1
### Changed
- All callbacks now receive a `Message` object. The `Payload` can be accessed using `message.payload`
### Added
- `channel.join()` can now take optional params to override the ones set while creating the Channel
- Timeouts when sending messages
- Rejoin timer which can be configured to attempt to rejoin given a function. Defaults to 1s, 2s, 5s, 10s and then retries every 10s
- Socket and Channel `on` callbacks are able to hold more than just a single callback
Thanks to @murphb52 and @ALucasVanDongen for helping with some of the development and testing of this release!
## [0.8.1]
### Fixed
- Initial params are not sent through when opening a channel
## [0.8.0]
### Updated
- Starscream to 3.0.4
- Swift 4
- Mirror [Phoenix.js](https://hexdocs.pm/phoenix/js/) more closely
[Unreleased]: https://github.com/davidstump/SwiftPhoenixClient/compare/1.3.0...HEAD
[1.3.0]: https://github.com/davidstump/SwiftPhoenixClient/compare/1.2.1...1.3.0
[1.2.1]: https://github.com/davidstump/SwiftPhoenixClient/compare/1.2.0...1.2.1
[0.9.3]: https://github.com/davidstump/SwiftPhoenixClient/compare/0.9.2...0.9.3
[0.9.2]: https://github.com/davidstump/SwiftPhoenixClient/compare/0.9.1...0.9.2
[0.9.1]: https://github.com/davidstump/SwiftPhoenixClient/compare/0.9.0...0.9.1
[0.9.0]: https://github.com/davidstump/SwiftPhoenixClient/compare/0.8.1...0.9.0
[0.8.1]: https://github.com/davidstump/SwiftPhoenixClient/compare/0.8.0...0.8.1
[0.8.0]: https://github.com/davidstump/SwiftPhoenixClient/compare/0.6.0...0.8.0
[migration guide]: https://github.com/davidstump/SwiftPhoenixClient/wiki/Usage-Guide
================================================
FILE: Cartfile.private
================================================
github "Quick/Quick" ~> 4.0.0
github "Quick/Nimble" ~> 9.0.0
================================================
FILE: Cartfile.resolved
================================================
github "Quick/Nimble" "v9.2.1"
github "Quick/Quick" "v4.0.0"
================================================
FILE: Examples/Basic/AppDelegate.swift
================================================
//
// AppDelegate.swift
// Basic
//
// Created by Daniel Rees on 10/23/20.
// Copyright © 2021 SwiftPhoenixClient. All rights reserved.
//
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
}
================================================
FILE: Examples/Basic/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Examples/Basic/Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Examples/Basic/Base.lproj/LaunchScreen.storyboard
================================================
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>
================================================
FILE: Examples/Basic/Base.lproj/Main.storyboard
================================================
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Zsx-Hf-BVQ">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Navigation Controller-->
<scene sceneID="hbi-sE-0YT">
<objects>
<navigationController navigationBarHidden="YES" id="Zsx-Hf-BVQ" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="T9Y-2W-aAw">
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="BYZ-38-t0r" kind="relationship" relationship="rootViewController" id="lvb-Xw-qkE"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="QOY-EI-0Lu" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-658" y="139"/>
</scene>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController storyboardIdentifier="vc.intro.basic" id="BYZ-38-t0r" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Basic" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="BEe-vt-MEe">
<rect key="frame" x="20" y="64" width="102.5" height="53"/>
<fontDescription key="fontDescription" type="system" pointSize="44"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Basic serves as a playground for testing various use cases of the Client. For a more robust usage, see the Chat Example app." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="h5t-Lj-irh">
<rect key="frame" x="20" y="133" width="374" height="61"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Chat Room" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="F6P-j3-jMr">
<rect key="frame" x="20" y="375" width="210" height="53"/>
<fontDescription key="fontDescription" type="system" pointSize="44"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Chat Room is a fully functional chat room that targets dwyl/phoenix-chat-example. It is a more complete implementation" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1ld-X6-Yo3">
<rect key="frame" x="20" y="444" width="374" height="61"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="UIR-BI-BRU">
<rect key="frame" x="91" y="254" width="232" height="41"/>
<fontDescription key="fontDescription" type="system" pointSize="24"/>
<state key="normal" title="Launch Basic Example"/>
<connections>
<segue destination="kvj-CR-pCE" kind="show" id="E54-7t-Dj4"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="b0l-Xq-l0D">
<rect key="frame" x="61" y="565" width="292" height="41"/>
<fontDescription key="fontDescription" type="system" pointSize="24"/>
<state key="normal" title="Launch Chat Room Example"/>
<connections>
<segue destination="RJS-iX-oRo" kind="show" id="g9S-Mr-CAq"/>
</connections>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="h5t-Lj-irh" secondAttribute="trailing" constant="20" id="6RQ-YM-Fyw"/>
<constraint firstItem="h5t-Lj-irh" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" constant="20" id="Fjb-7q-bZP"/>
<constraint firstItem="F6P-j3-jMr" firstAttribute="top" secondItem="UIR-BI-BRU" secondAttribute="bottom" constant="80" id="Fn6-hq-Sdx"/>
<constraint firstItem="UIR-BI-BRU" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="H2Q-7g-Vi4"/>
<constraint firstItem="F6P-j3-jMr" firstAttribute="leading" secondItem="BEe-vt-MEe" secondAttribute="leading" id="HeP-E4-rIE"/>
<constraint firstItem="1ld-X6-Yo3" firstAttribute="trailing" secondItem="h5t-Lj-irh" secondAttribute="trailing" id="ItY-dT-IhA"/>
<constraint firstItem="h5t-Lj-irh" firstAttribute="top" secondItem="BEe-vt-MEe" secondAttribute="bottom" constant="16" id="OgN-X9-efj"/>
<constraint firstItem="b0l-Xq-l0D" firstAttribute="top" secondItem="1ld-X6-Yo3" secondAttribute="bottom" constant="60" id="ZL6-ca-iFm"/>
<constraint firstItem="1ld-X6-Yo3" firstAttribute="top" secondItem="F6P-j3-jMr" secondAttribute="bottom" constant="16" id="dco-DZ-iog"/>
<constraint firstItem="b0l-Xq-l0D" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="fln-MG-sWG"/>
<constraint firstItem="1ld-X6-Yo3" firstAttribute="leading" secondItem="h5t-Lj-irh" secondAttribute="leading" id="ib5-mu-7cV"/>
<constraint firstItem="UIR-BI-BRU" firstAttribute="top" secondItem="h5t-Lj-irh" secondAttribute="bottom" constant="60" id="u1I-Bv-Dki"/>
<constraint firstItem="BEe-vt-MEe" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" constant="20" id="xZp-Ju-TpI"/>
<constraint firstItem="BEe-vt-MEe" firstAttribute="top" secondItem="6Tk-OE-BBY" secondAttribute="top" constant="20" id="yRM-vL-Fnf"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="vrV-LP-NAa"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="131.8840579710145" y="138.61607142857142"/>
</scene>
<!--Basic Chat View Controller-->
<scene sceneID="fHJ-wR-6Fv">
<objects>
<viewController id="kvj-CR-pCE" customClass="BasicChatViewController" customModule="Basic" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="r7Y-Xw-V91">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Basic" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bzZ-eQ-71I">
<rect key="frame" x="20" y="64" width="103" height="53"/>
<fontDescription key="fontDescription" type="system" pointSize="44"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Message..." textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="8vj-8R-JAk">
<rect key="frame" x="20" y="814" width="306" height="40"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits"/>
</textField>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="aCb-6h-kVE">
<rect key="frame" x="334" y="814" width="60" height="40"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="5Hw-T3-ZT0"/>
<constraint firstAttribute="width" constant="60" id="b7Q-yS-ZZE"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<state key="normal" title="Send"/>
<connections>
<action selector="sendMessage:" destination="kvj-CR-pCE" eventType="touchUpInside" id="mVm-qe-579"/>
</connections>
</button>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="KF5-Gt-2Qg">
<rect key="frame" x="20" y="125" width="374" height="681"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="slt-ax-j8X">
<rect key="frame" x="319" y="72.5" width="75" height="36"/>
<fontDescription key="fontDescription" type="system" pointSize="20"/>
<state key="normal" title="Connect"/>
<connections>
<action selector="onConnectButtonPressed:" destination="kvj-CR-pCE" eventType="touchUpInside" id="01e-UK-Kon"/>
</connections>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="aNG-mQ-eOn"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="aCb-6h-kVE" firstAttribute="top" secondItem="8vj-8R-JAk" secondAttribute="top" id="0jQ-EO-UqX"/>
<constraint firstItem="8vj-8R-JAk" firstAttribute="top" secondItem="KF5-Gt-2Qg" secondAttribute="bottom" constant="8" id="1L9-aq-gIU"/>
<constraint firstItem="slt-ax-j8X" firstAttribute="centerY" secondItem="bzZ-eQ-71I" secondAttribute="centerY" id="FJQ-4m-Tbw"/>
<constraint firstItem="bzZ-eQ-71I" firstAttribute="top" secondItem="aNG-mQ-eOn" secondAttribute="top" constant="20" id="IUb-ZL-Q3S"/>
<constraint firstItem="aNG-mQ-eOn" firstAttribute="trailing" secondItem="aCb-6h-kVE" secondAttribute="trailing" constant="20" id="JxZ-Gw-Sf5"/>
<constraint firstItem="8vj-8R-JAk" firstAttribute="leading" secondItem="aNG-mQ-eOn" secondAttribute="leading" constant="20" id="Kcw-3w-5CD"/>
<constraint firstItem="aNG-mQ-eOn" firstAttribute="bottom" secondItem="aCb-6h-kVE" secondAttribute="bottom" constant="8" id="LoR-eB-PPJ"/>
<constraint firstItem="bzZ-eQ-71I" firstAttribute="leading" secondItem="aNG-mQ-eOn" secondAttribute="leading" constant="20" id="UC4-bp-6Uz"/>
<constraint firstItem="aNG-mQ-eOn" firstAttribute="trailing" secondItem="slt-ax-j8X" secondAttribute="trailing" constant="20" id="XVS-iI-8CF"/>
<constraint firstItem="aCb-6h-kVE" firstAttribute="bottom" secondItem="8vj-8R-JAk" secondAttribute="bottom" id="jsR-RR-Ngx"/>
<constraint firstItem="aCb-6h-kVE" firstAttribute="leading" secondItem="8vj-8R-JAk" secondAttribute="trailing" constant="8" id="nBm-Zz-oW2"/>
<constraint firstItem="KF5-Gt-2Qg" firstAttribute="leading" secondItem="aNG-mQ-eOn" secondAttribute="leading" constant="20" id="oxs-ll-OVi"/>
<constraint firstItem="aNG-mQ-eOn" firstAttribute="trailing" secondItem="KF5-Gt-2Qg" secondAttribute="trailing" constant="20" id="rBf-m4-fTZ"/>
<constraint firstItem="KF5-Gt-2Qg" firstAttribute="top" secondItem="bzZ-eQ-71I" secondAttribute="bottom" constant="8" id="yiO-mz-wj6"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="nZS-Fg-Mad"/>
<connections>
<outlet property="chatWindow" destination="KF5-Gt-2Qg" id="t4M-l4-Mww"/>
<outlet property="connectButton" destination="slt-ax-j8X" id="z5A-IX-s74"/>
<outlet property="messageField" destination="8vj-8R-JAk" id="QU5-Ub-eek"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="l8U-lP-vWR" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="936" y="-92"/>
</scene>
<!--Chat Room View Controller-->
<scene sceneID="1Zq-kN-hM5">
<objects>
<viewController id="RJS-iX-oRo" customClass="ChatRoomViewController" customModule="Basic" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="yzJ-nT-Ptm">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Chat Room" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AeE-bA-VFh">
<rect key="frame" x="20" y="64" width="210" height="53"/>
<fontDescription key="fontDescription" type="system" pointSize="44"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Message..." textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="98Q-7Z-8K2">
<rect key="frame" x="20" y="814" width="306" height="40"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits"/>
</textField>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="zEg-l5-c0O">
<rect key="frame" x="334" y="814" width="60" height="40"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="9Hk-Vp-RDS"/>
<constraint firstAttribute="width" constant="60" id="qwQ-j4-YWI"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<state key="normal" title="Send"/>
<connections>
<action selector="onSendButtonPressed:" destination="RJS-iX-oRo" eventType="touchUpInside" id="mpC-eD-smW"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rRW-yN-Yh6">
<rect key="frame" x="362" y="72.5" width="32" height="36"/>
<fontDescription key="fontDescription" type="system" pointSize="20"/>
<state key="normal" title="Exit"/>
<connections>
<action selector="onExitButtonPressed:" destination="RJS-iX-oRo" eventType="touchUpInside" id="vRM-5t-ull"/>
</connections>
</button>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="plain" separatorStyle="none" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="H5w-0M-Z0D">
<rect key="frame" x="0.0" y="125" width="414" height="681"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</tableView>
</subviews>
<viewLayoutGuide key="safeArea" id="MAx-vC-OCj"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="AeE-bA-VFh" firstAttribute="top" secondItem="MAx-vC-OCj" secondAttribute="top" constant="20" id="Fzh-cn-F6n"/>
<constraint firstItem="zEg-l5-c0O" firstAttribute="top" secondItem="98Q-7Z-8K2" secondAttribute="top" id="M7H-aj-AtL"/>
<constraint firstItem="98Q-7Z-8K2" firstAttribute="top" secondItem="H5w-0M-Z0D" secondAttribute="bottom" constant="8" id="MiZ-bC-UOQ"/>
<constraint firstItem="zEg-l5-c0O" firstAttribute="bottom" secondItem="98Q-7Z-8K2" secondAttribute="bottom" id="TfB-TF-zUj"/>
<constraint firstItem="rRW-yN-Yh6" firstAttribute="centerY" secondItem="AeE-bA-VFh" secondAttribute="centerY" id="Uz9-N1-etM"/>
<constraint firstItem="MAx-vC-OCj" firstAttribute="bottom" secondItem="zEg-l5-c0O" secondAttribute="bottom" constant="8" id="Yml-Pj-bcg"/>
<constraint firstItem="H5w-0M-Z0D" firstAttribute="top" secondItem="AeE-bA-VFh" secondAttribute="bottom" constant="8" id="c2Y-Ic-eQW"/>
<constraint firstItem="MAx-vC-OCj" firstAttribute="trailing" secondItem="H5w-0M-Z0D" secondAttribute="trailing" id="d8n-Gf-e7g"/>
<constraint firstItem="98Q-7Z-8K2" firstAttribute="leading" secondItem="MAx-vC-OCj" secondAttribute="leading" constant="20" id="fSG-ft-bc9"/>
<constraint firstItem="H5w-0M-Z0D" firstAttribute="leading" secondItem="MAx-vC-OCj" secondAttribute="leading" id="fkL-bu-6ft"/>
<constraint firstItem="MAx-vC-OCj" firstAttribute="trailing" secondItem="rRW-yN-Yh6" secondAttribute="trailing" constant="20" id="gb1-Mk-q4f"/>
<constraint firstItem="MAx-vC-OCj" firstAttribute="trailing" secondItem="zEg-l5-c0O" secondAttribute="trailing" constant="20" id="gvo-XL-cDQ"/>
<constraint firstItem="zEg-l5-c0O" firstAttribute="leading" secondItem="98Q-7Z-8K2" secondAttribute="trailing" constant="8" id="iDb-I1-mhP"/>
<constraint firstItem="AeE-bA-VFh" firstAttribute="leading" secondItem="MAx-vC-OCj" secondAttribute="leading" constant="20" id="nY4-Wj-yBw"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="TlM-WA-si1"/>
<connections>
<outlet property="messageInput" destination="98Q-7Z-8K2" id="Gmh-GX-fqq"/>
<outlet property="tableView" destination="H5w-0M-Z0D" id="Fde-iv-hx8"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="e7P-lT-1aG" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="935" y="561"/>
</scene>
</scenes>
<resources>
<systemColor name="labelColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
================================================
FILE: Examples/Basic/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
================================================
FILE: Examples/Basic/SceneDelegate.swift
================================================
//
// SceneDelegate.swift
// Basic
//
// Created by Daniel Rees on 10/23/20.
// Copyright © 2021 SwiftPhoenixClient. All rights reserved.
//
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
}
================================================
FILE: Examples/Basic/basic/BasicChatViewController.swift
================================================
//
// BasicChatViewController.swift
// Basic
//
// Created by Daniel Rees on 10/23/20.
// Copyright © 2021 SwiftPhoenixClient. All rights reserved.
//
import UIKit
import SwiftPhoenixClient
/*
Testing with Basic Chat
This class is designed to provide as a sandbox to test various client features in a "real"
environment where network can be dropped, disconnected, servers can quit, etc.
For a more advanced example, see the ChatRoomViewController
This example is intended to connect to a local chat server.
Setup
1. Select which Transpart is being tested.
Steps
1. Connect the Socket
2. Verify System pings come through
3. Send a message and verify it is returned by the server
4. From a web client, send a message and verify it is received by the app.
5. Disconnect and Connect the Socket again
6. Kill the server, verifying that the retry starts
7. Start the server again, verifying that the client reconnects
8. After the client reconnects, verify pings and messages work as before
9. Disconnect the client and kill the server again
10. While the server is disconnected, connect the client
11. Start the server and verify that the client connects once the server is available
*/
let endpoint = "http://localhost:4000/socket/websocket"
class BasicChatViewController: UIViewController {
// MARK: - Child Views
@IBOutlet weak var connectButton: UIButton!
@IBOutlet weak var messageField: UITextField!
@IBOutlet weak var chatWindow: UITextView!
// MARK: - Variables
let username: String = "Basic"
var topic: String = "rooms:lobby"
// Test the URLSessionTransport
let socket = Socket(endpoint)
var lobbyChannel: Channel!
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.chatWindow.text = ""
// To automatically manage retain cycles, use `delegate*(to:)` methods.
// If you would prefer to handle them yourself, youcan use the same
// methods without the `delegate` functions, just be sure you avoid
// memory leakse with `[weak self]`
socket.delegateOnOpen(to: self) { (self) in
self.addText("Socket Opened")
DispatchQueue.main.async {
self.connectButton.setTitle("Disconnect", for: .normal)
}
}
socket.delegateOnClose(to: self) { (self) in
self.addText("Socket Closed")
DispatchQueue.main.async {
self.connectButton.setTitle("Connect", for: .normal)
}
}
socket.delegateOnError(to: self) { (self, arg1) in
let (error, response) = arg1
if let statusCode = (response as? HTTPURLResponse)?.statusCode, statusCode > 400 {
self.addText("Socket Errored: \(statusCode)")
self.socket.disconnect()
} else {
self.addText("Socket Errored: " + error.localizedDescription)
}
}
socket.logger = { msg in print("LOG:", msg) }
}
// MARK: - IBActions
@IBAction func onConnectButtonPressed(_ sender: Any) {
if socket.isConnected {
disconnectAndLeave()
} else {
connectAndJoin()
}
}
@IBAction func sendMessage(_ sender: UIButton) {
let payload = ["user": username, "body": messageField.text!]
self.lobbyChannel
.push("new:msg", payload: payload)
.receive("ok") { (message) in
print("success", message)
}
.receive("error") { (errorMessage) in
print("error: ", errorMessage)
}
messageField.text = ""
}
private func disconnectAndLeave() {
// Be sure the leave the channel or call socket.remove(lobbyChannel)
lobbyChannel.leave()
socket.disconnect {
self.addText("Socket Disconnected")
}
}
private func connectAndJoin() {
let channel = socket.channel(topic, params: ["status":"joining"])
channel.delegateOn("join", to: self) { (self, _) in
self.addText("You joined the room.")
}
channel.delegateOn("new:msg", to: self) { (self, message) in
let payload = message.payload
guard
let username = payload["user"],
let body = payload["body"] else { return }
let newMessage = "[\(username)] \(body)"
self.addText(newMessage)
}
channel.delegateOn("user:entered", to: self) { (self, message) in
self.addText("[anonymous entered]")
}
self.lobbyChannel = channel
self.lobbyChannel
.join()
.delegateReceive("ok", to: self) { (self, _) in
self.addText("Joined Channel")
}.delegateReceive("error", to: self) { (self, message) in
self.addText("Failed to join channel: \(message.payload)")
}
self.socket.connect()
}
private func addText(_ text: String) {
DispatchQueue.main.async {
let updatedText = self.chatWindow.text.appending(text).appending("\n")
self.chatWindow.text = updatedText
let bottom = NSMakeRange(updatedText.count - 1, 1)
self.chatWindow.scrollRangeToVisible(bottom)
}
}
}
================================================
FILE: Examples/Basic/chatroom/ChatRoomViewController.swift
================================================
//
// ChatRoomViewController.swift
// Basic
//
// Created by Daniel Rees on 12/22/20.
// Copyright © 2021 SwiftPhoenixClient. All rights reserved.
//
import UIKit
import SwiftPhoenixClient
struct Shout {
let name: String
let message: String
}
/*
ChatRoom provides a "real" example of using SwiftPhoenixClient, including how
to use the Rx extensions for it. It also utilizes logic to disconnect/reconnect
the socket when the app enters and exits the foreground.
NOTE: iOS can, at will, kill your connection if the app enters the background without
notiftying your process that it has been killed. Thus resulting in a disconnected
socket when the app comes back to the foreground. The best way around this is to
listen to UIApplication.didBecomeActiveNotification events and manually check if the socket is still connected
and attempt to reconnect and rejoin any channels.
In this example, the channel is left and socket is disconnected when the app enters
the background and then a new channel is created and joined and the socket is connected
when the app enters the foreground.
This example utilizes the PhxChat example at https://github.com/dwyl/phoenix-chat-example
*/
class ChatRoomViewController: UIViewController {
// MARK: - Child Views
@IBOutlet weak var messageInput: UITextField!
@IBOutlet weak var tableView: UITableView!
// MARK: - Attributes
private let username: String = "ChatRoom"
// private let socket = Socket("http://localhost:4000/socket/websocket")
private let socket = Socket("https://phoenix-chat.fly.dev/socket/websocket")
private let topic: String = "room:lobby"
private var lobbyChannel: Channel?
private var shouts: [Shout] = []
// Notifcation Subscriptions
private var didbecomeActiveObservervation: NSObjectProtocol?
private var willResignActiveObservervation: NSObjectProtocol?
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.dataSource = self
// When app enters foreground, be sure that the socket is connected
self.observeDidBecomeActive()
// Connect to the chat for the first time
self.connectToChat()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// When the Controller is removed from the view hierarchy, then stop
// observing app lifecycle and disconnect from the chat
self.removeAppActiveObservation()
self.disconnectFromChat()
}
// MARK: - IB Actions
@IBAction func onExitButtonPressed(_ sender: Any) {
self.navigationController?.popViewController(animated: true)
}
@IBAction func onSendButtonPressed(_ sender: Any) {
// Create and send the payload
let payload = ["name": username, "message": messageInput.text!]
self.lobbyChannel?.push("shout", payload: payload)
// Clear the text intput
self.messageInput.text = ""
}
//----------------------------------------------------------------------
// MARK: - Background/Foreground reconnect strategy
//----------------------------------------------------------------------
private func observeDidBecomeActive() {
//Make sure there's no other observations
self.removeAppActiveObservation()
self.didbecomeActiveObservervation = NotificationCenter.default
.addObserver(forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: .main) { [weak self] _ in self?.connectToChat() }
// When the app resigns being active, the leave any existing channels
// and disconnect from the websocket.
self.willResignActiveObservervation = NotificationCenter.default
.addObserver(forName: UIApplication.willResignActiveNotification,
object: nil,
queue: .main) { [weak self] _ in self?.disconnectFromChat() }
}
private func removeAppActiveObservation() {
if let observer = self.didbecomeActiveObservervation {
NotificationCenter.default.removeObserver(observer)
self.didbecomeActiveObservervation = nil
}
if let observer = self.willResignActiveObservervation {
NotificationCenter.default.removeObserver(observer)
self.willResignActiveObservervation = nil
}
}
private func connectToChat() {
// Setup the socket to receive open/close events
socket.delegateOnOpen(to: self) { (self) in
print("CHAT ROOM: Socket Opened")
}
socket.delegateOnClose(to: self) { (self) in
print("CHAT ROOM: Socket Closed")
}
socket.delegateOnError(to: self) { (self, error) in
let (error, response) = error
if let statusCode = (response as? HTTPURLResponse)?.statusCode, statusCode > 400 {
print("CHAT ROOM: Socket Errored. \(statusCode)")
self.socket.disconnect()
} else {
print("CHAT ROOM: Socket Errored. \(error)")
}
}
socket.logger = { msg in print("LOG:", msg) }
// Setup the Channel to receive and send messages
let channel = socket.channel(topic, params: ["status": "joining"])
channel.delegateOn("shout", to: self) { (self, message) in
let payload = message.payload
guard
let name = payload["name"] as? String,
let message = payload["message"] as? String else { return }
let shout = Shout(name: name, message: message)
self.shouts.append(shout)
DispatchQueue.main.async {
let indexPath = IndexPath(row: self.shouts.count - 1, section: 0)
self.tableView.reloadData() //reloadRows(at: [indexPath], with: .automatic)
self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
// Now connect the socket and join the channel
self.lobbyChannel = channel
self.lobbyChannel?
.join()
.delegateReceive("ok", to: self, callback: { (self, _) in
print("CHANNEL: rooms:lobby joined")
})
.delegateReceive("error", to: self, callback: { (self, message) in
print("CHANNEL: rooms:lobby failed to join. \(message.payload)")
})
self.socket.connect()
}
private func disconnectFromChat() {
if let channel = self.lobbyChannel {
channel.leave()
self.socket.remove(channel)
}
self.socket.disconnect()
self.shouts = []
}
}
extension ChatRoomViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.shouts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "shout_cell")
let shout = self.shouts[indexPath.row]
cell.textLabel?.text = shout.message
cell.detailTextLabel?.text = shout.name
return cell
}
}
================================================
FILE: Gemfile
================================================
source "https://rubygems.org"
gem 'fastlane'
gem 'slather'
================================================
FILE: LICENSE
================================================
Copyright (c) 2015 David Stump <david@davidstump.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: Package.swift
================================================
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "SwiftPhoenixClient",
platforms: [
.macOS(.v10_12),
.iOS(.v10),
.tvOS(.v10),
.watchOS(.v3)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(name: "SwiftPhoenixClient", targets: ["SwiftPhoenixClient"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "SwiftPhoenixClient",
dependencies: []
),
.testTarget(
name: "SwiftPhoenixClientTests",
dependencies: ["SwiftPhoenixClient"]),
]
)
================================================
FILE: README.md
================================================
# Swift Phoenix Client
[](https://swift.org/)
[](http://cocoapods.org/pods/SwiftPhoenixClient)
[](http://cocoapods.org/pods/SwiftPhoenixClient)
[](http://cocoapods.org/pods/SwiftPhoenixClient)
[](https://github.com/Carthage/Carthage)
[](https://www.codetriage.com/davidstump/swiftphoenixclient)
## About
SwiftPhoenixClient is a Swift port of phoenix.js, allowing your swift projects
to connect to a Phoenix Websocket backend.
We try out best to keep the library up to date with phoenix.js but if there is
something that is missing, please create an issue or, even better, a PR to
address the change.
## Sample Projects
You can view the example of how to use SwiftPhoenixClient in the Example/ dir.
There are two primary classes, `BasicViewController` and `ChatRoomViewController`.
The `BasicViewController` is designed to test against a [local chat server](https://github.com/chrismccord/phoenix_chat_example)
where as `ChatRoomViewController` is a more "complete" example which targets
dwyl's [phoenix-chat-example](https://github.com/dwyl/phoenix-chat-example) Heroku app.
### SwiftPhoenixClient
The core module which provides the Phoenix Channels and Presence logic. It also
uses URLSession's default WebSocket implementation which has a minimum iOS target
of 13.0.
## Installation
### CocoaPods
You can install SwiftPhoenix Client via CocoaPods by adding the following to your
Podfile. Keep in mind that in order to use Swift Phoenix Client, the minimum iOS
target must be '9.0'
```RUBY
pod "SwiftPhoenixClient", '~> 5.3'
```
and running `pod install`. From there you will need to add `import SwiftPhoenixClient` in any class you want it to be used.
### Carthage
If you use Carthage to manage your dependencies, simply add
SwiftPhoenixClient to your `Cartfile`:
```
github "davidstump/SwiftPhoenixClient" ~> 5.3
```
Then run `carthage update`.
If this is your first time using Carthage in the project, you'll need to go through some additional steps as explained [over at Carthage](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).
### SwiftPackageManager
_Note: Instructions below are for using **SwiftPM** without the Xcode UI. It's the easiest to go to your Project Settings -> Swift Packages and add SwiftPhoenixClient from there._
To integrate using Apple's Swift package manager, without Xcode integration, add the following as a dependency to your `Package.swift`:
```swift
.package(url: "https://github.com/davidstump/SwiftPhoenixClient.git", .upToNextMajor(from: "5.2.2"))
```
and then specify `"SwiftPhoenixClient"` as a dependency of the Target in which you wish to use SwiftPhoenixClient.
## Usage
Using the Swift Phoenix Client is extremely easy (and familiar if you have used the phoenix.js client).
See the [Usage Guide](https://github.com/davidstump/SwiftPhoenixClient/wiki/Usage-Guide) for details instructions. You can also check out the [documentation](http://davidstump.github.io/SwiftPhoenixClient/)
## Example
Check out the [ViewController](https://github.com/davidstump/SwiftPhoenixClient/blob/master/Examples/Basic/chatroom/ChatRoomViewController.swift) in this repo for a brief example of a simple iOS chat application using the [Phoenix Chat Example](https://github.com/dwyl/phoenix-chat-example)
Also check out both the Swift and Elixir channels on IRC.
## Development
Check out the wiki page for [getting started](https://github.com/davidstump/SwiftPhoenixClient/wiki/Contributing)
## Thanks
Many many thanks to [Daniel Rees](https://github.com/dsrees) for his many contributions and continued maintenance of this project!
## License
SwiftPhoenixClient is available under the MIT license. See the LICENSE file for more info.
================================================
FILE: RELEASING.md
================================================
Release Process
===============
1. Ensure `version` in `SwiftPhoenixClient.podsec` is set to the version you want to release.
2. Run a trial pod release `pod lib lint`
3. Update `CHANGELOG.md` with the version about to be released along with notes.
4. Commit: `git commit -am "Prepare version X.Y.X"`
5. Tag: `git tag -a X.Y.Z -m "Version X.Y.Z"`
6. Push: `git push && git push --tags`
7. Release to Cocoapods `pod trunk push SwiftPhoenixClient.podspec`
8. Add the new release with notes (https://github.com/davidstump/SwiftPhoenixClient/releases).
================================================
FILE: Sources/Supporting Files/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2021 SwiftPhoenixClient. All rights reserved.</string>
</dict>
</plist>
================================================
FILE: Sources/Supporting Files/SwiftPhoenixClient.h
================================================
//
// SwiftPhoenixClient.h
// SwiftPhoenixClient
//
// Created by Daniel Rees on 10/21/20.
// Copyright © 2021 SwiftPhoenixClient. All rights reserved.
//
#import <Foundation/Foundation.h>
//! Project version number for SwiftPhoenixClient.
FOUNDATION_EXPORT double SwiftPhoenixClientVersionNumber;
//! Project version string for SwiftPhoenixClient.
FOUNDATION_EXPORT const unsigned char SwiftPhoenixClientVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <SwiftPhoenixClient/PublicHeader.h>
================================================
FILE: Sources/SwiftPhoenixClient/Channel.swift
================================================
// Copyright (c) 2021 David Stump <david@davidstump.net>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Swift
import Foundation
/// Container class of bindings to the channel
struct Binding {
// The event that the Binding is bound to
let event: String
// The reference number of the Binding
let ref: Int
// The callback to be triggered
let callback: Delegated<Message, Void>
}
///
/// Represents a Channel which is bound to a topic
///
/// A Channel can bind to multiple events on a given topic and
/// be informed when those events occur within a topic.
///
/// ### Example:
///
/// let channel = socket.channel("room:123", params: ["token": "Room Token"])
/// channel.on("new_msg") { payload in print("Got message", payload") }
/// channel.push("new_msg, payload: ["body": "This is a message"])
/// .receive("ok") { payload in print("Sent message", payload) }
/// .receive("error") { payload in print("Send failed", payload) }
/// .receive("timeout") { payload in print("Networking issue...", payload) }
///
/// channel.join()
/// .receive("ok") { payload in print("Channel Joined", payload) }
/// .receive("error") { payload in print("Failed ot join", payload) }
/// .receive("timeout") { payload in print("Networking issue...", payload) }
///
import Foundation
public class Channel {
/// The topic of the Channel. e.g. "rooms:friends"
public let topic: String
/// The params sent when joining the channel
public var params: Payload {
didSet { self.joinPush.payload = params }
}
/// The Socket that the channel belongs to
weak var socket: Socket?
/// Current state of the Channel
var state: ChannelState
/// Collection of event bindings
let syncBindingsDel: SynchronizedArray<Binding>
/// Tracks event binding ref counters
var bindingRef: Int
/// Timout when attempting to join a Channel
var timeout: TimeInterval
/// Set to true once the channel calls .join()
var joinedOnce: Bool
/// Push to send when the channel calls .join()
var joinPush: Push!
/// Buffer of Pushes that will be sent once the Channel's socket connects
var pushBuffer: [Push]
/// Timer to attempt to rejoin
var rejoinTimer: TimeoutTimer
/// Refs of stateChange hooks
var stateChangeRefs: [String]
/// Initialize a Channel
///
/// - parameter topic: Topic of the Channel
/// - parameter params: Optional. Parameters to send when joining.
/// - parameter socket: Socket that the channel is a part of
init(topic: String, params: [String: Any] = [:], socket: Socket) {
self.state = ChannelState.closed
self.topic = topic
self.params = params
self.socket = socket
self.syncBindingsDel = SynchronizedArray()
self.bindingRef = 0
self.timeout = socket.timeout
self.joinedOnce = false
self.pushBuffer = []
self.stateChangeRefs = []
self.rejoinTimer = TimeoutTimer()
// Setup Timer delgation
self.rejoinTimer.callback
.delegate(to: self) { (self) in
if self.socket?.isConnected == true { self.rejoin() }
}
self.rejoinTimer.timerCalculation
.delegate(to: self) { (self, tries) -> TimeInterval in
return self.socket?.rejoinAfter(tries) ?? 5.0
}
// Respond to socket events
let onErrorRef = self.socket?.delegateOnError(to: self, callback: { (self, _) in
self.rejoinTimer.reset()
})
if let ref = onErrorRef { self.stateChangeRefs.append(ref) }
let onOpenRef = self.socket?.delegateOnOpen(to: self, callback: { (self) in
self.rejoinTimer.reset()
if (self.isErrored) { self.rejoin() }
})
if let ref = onOpenRef { self.stateChangeRefs.append(ref) }
// Setup Push Event to be sent when joining
self.joinPush = Push(channel: self,
event: ChannelEvent.join,
payload: self.params,
timeout: self.timeout)
/// Handle when a response is received after join()
self.joinPush.delegateReceive("ok", to: self) { (self, _) in
// Mark the Channel as joined
self.state = ChannelState.joined
// Reset the timer, preventing it from attempting to join again
self.rejoinTimer.reset()
// Send and buffered messages and clear the buffer
self.pushBuffer.forEach( { $0.send() })
self.pushBuffer = []
}
// Perform if Channel errors while attempting to joi
self.joinPush.delegateReceive("error", to: self) { (self, _) in
self.state = .errored
if (self.socket?.isConnected == true) { self.rejoinTimer.scheduleTimeout() }
}
// Handle when the join push times out when sending after join()
self.joinPush.delegateReceive("timeout", to: self) { (self, _) in
// log that the channel timed out
self.socket?.logItems("channel", "timeout \(self.topic) \(self.joinRef ?? "") after \(self.timeout)s")
// Send a Push to the server to leave the channel
let leavePush = Push(channel: self,
event: ChannelEvent.leave,
timeout: self.timeout)
leavePush.send()
// Mark the Channel as in an error and attempt to rejoin if socket is connected
self.state = ChannelState.errored
self.joinPush.reset()
if (self.socket?.isConnected == true) { self.rejoinTimer.scheduleTimeout() }
}
/// Perfom when the Channel has been closed
self.delegateOnClose(to: self) { (self, _) in
// Reset any timer that may be on-going
self.rejoinTimer.reset()
// Log that the channel was left
self.socket?.logItems("channel", "close topic: \(self.topic) joinRef: \(self.joinRef ?? "nil")")
// Mark the channel as closed and remove it from the socket
self.state = ChannelState.closed
self.socket?.remove(self)
}
/// Perfom when the Channel errors
self.delegateOnError(to: self) { (self, message) in
// Log that the channel received an error
self.socket?.logItems("channel", "error topic: \(self.topic) joinRef: \(self.joinRef ?? "nil") mesage: \(message)")
// If error was received while joining, then reset the Push
if (self.isJoining) {
// Make sure that the "phx_join" isn't buffered to send once the socket
// reconnects. The channel will send a new join event when the socket connects.
if let safeJoinRef = self.joinRef {
self.socket?.removeFromSendBuffer(ref: safeJoinRef)
}
// Reset the push to be used again later
self.joinPush.reset()
}
// Mark the channel as errored and attempt to rejoin if socket is currently connected
self.state = ChannelState.errored
if (self.socket?.isConnected == true) { self.rejoinTimer.scheduleTimeout() }
}
// Perform when the join reply is received
self.delegateOn(ChannelEvent.reply, to: self) { (self, message) in
// Trigger bindings
self.trigger(event: self.replyEventName(message.ref),
payload: message.rawPayload,
ref: message.ref,
joinRef: message.joinRef)
}
}
deinit {
rejoinTimer.reset()
}
/// Overridable message hook. Receives all events for specialized message
/// handling before dispatching to the channel callbacks.
///
/// - parameter msg: The Message received by the client from the server
/// - return: Must return the message, modified or unmodified
public var onMessage: (_ message: Message) -> Message = { (message) in
return message
}
/// Joins the channel
///
/// - parameter timeout: Optional. Defaults to Channel's timeout
/// - return: Push event
@discardableResult
public func join(timeout: TimeInterval? = nil) -> Push {
guard !joinedOnce else {
fatalError("tried to join multiple times. 'join' "
+ "can only be called a single time per channel instance")
}
// Join the Channel
if let safeTimeout = timeout { self.timeout = safeTimeout }
self.joinedOnce = true
self.rejoin()
return joinPush
}
/// Hook into when the Channel is closed. Does not handle retain cycles.
/// Use `delegateOnClose(to:)` for automatic handling of retain cycles.
///
/// Example:
///
/// let channel = socket.channel("topic")
/// channel.onClose() { [weak self] message in
/// self?.print("Channel \(message.topic) has closed"
/// }
///
/// - parameter callback: Called when the Channel closes
/// - return: Ref counter of the subscription. See `func off()`
@discardableResult
public func onClose(_ callback: @escaping ((Message) -> Void)) -> Int {
return self.on(ChannelEvent.close, callback: callback)
}
/// Hook into when the Channel is closed. Automatically handles retain
/// cycles. Use `onClose()` to handle yourself.
///
/// Example:
///
/// let channel = socket.channel("topic")
/// channel.delegateOnClose(to: self) { (self, message) in
/// self.print("Channel \(message.topic) has closed"
/// }
///
/// - parameter owner: Class registering the callback. Usually `self`
/// - parameter callback: Called when the Channel closes
/// - return: Ref counter of the subscription. See `func off()`
@discardableResult
public func delegateOnClose<Target: AnyObject>(to owner: Target,
callback: @escaping ((Target, Message) -> Void)) -> Int {
return self.delegateOn(ChannelEvent.close, to: owner, callback: callback)
}
/// Hook into when the Channel receives an Error. Does not handle retain
/// cycles. Use `delegateOnError(to:)` for automatic handling of retain
/// cycles.
///
/// Example:
///
/// let channel = socket.channel("topic")
/// channel.onError() { [weak self] (message) in
/// self?.print("Channel \(message.topic) has errored"
/// }
///
/// - parameter callback: Called when the Channel closes
/// - return: Ref counter of the subscription. See `func off()`
@discardableResult
public func onError(_ callback: @escaping ((_ message: Message) -> Void)) -> Int {
return self.on(ChannelEvent.error, callback: callback)
}
/// Hook into when the Channel receives an Error. Automatically handles
/// retain cycles. Use `onError()` to handle yourself.
///
/// Example:
///
/// let channel = socket.channel("topic")
/// channel.delegateOnError(to: self) { (self, message) in
/// self.print("Channel \(message.topic) has closed"
/// }
///
/// - parameter owner: Class registering the callback. Usually `self`
/// - parameter callback: Called when the Channel closes
/// - return: Ref counter of the subscription. See `func off()`
@discardableResult
public func delegateOnError<Target: AnyObject>(to owner: Target,
callback: @escaping ((Target, Message) -> Void)) -> Int {
return self.delegateOn(ChannelEvent.error, to: owner, callback: callback)
}
/// Subscribes on channel events. Does not handle retain cycles. Use
/// `delegateOn(_:, to:)` for automatic handling of retain cycles.
///
/// Subscription returns a ref counter, which can be used later to
/// unsubscribe the exact event listener
///
/// Example:
///
/// let channel = socket.channel("topic")
/// let ref1 = channel.on("event") { [weak self] (message) in
/// self?.print("do stuff")
/// }
/// let ref2 = channel.on("event") { [weak self] (message) in
/// self?.print("do other stuff")
/// }
/// channel.off("event", ref1)
///
/// Since unsubscription of ref1, "do stuff" won't print, but "do other
/// stuff" will keep on printing on the "event"
///
/// - parameter event: Event to receive
/// - parameter callback: Called with the event's message
/// - return: Ref counter of the subscription. See `func off()`
@discardableResult
public func on(_ event: String, callback: @escaping ((Message) -> Void)) -> Int {
var delegated = Delegated<Message, Void>()
delegated.manuallyDelegate(with: callback)
return self.on(event, delegated: delegated)
}
/// Subscribes on channel events. Automatically handles retain cycles. Use
/// `on()` to handle yourself.
///
/// Subscription returns a ref counter, which can be used later to
/// unsubscribe the exact event listener
///
/// Example:
///
/// let channel = socket.channel("topic")
/// let ref1 = channel.delegateOn("event", to: self) { (self, message) in
/// self?.print("do stuff")
/// }
/// let ref2 = channel.delegateOn("event", to: self) { (self, message) in
/// self?.print("do other stuff")
/// }
/// channel.off("event", ref1)
///
/// Since unsubscription of ref1, "do stuff" won't print, but "do other
/// stuff" will keep on printing on the "event"
///
/// - parameter event: Event to receive
/// - parameter owner: Class registering the callback. Usually `self`
/// - parameter callback: Called with the event's message
/// - return: Ref counter of the subscription. See `func off()`
@discardableResult
public func delegateOn<Target: AnyObject>(_ event: String,
to owner: Target,
callback: @escaping ((Target, Message) -> Void)) -> Int {
var delegated = Delegated<Message, Void>()
delegated.delegate(to: owner, with: callback)
return self.on(event, delegated: delegated)
}
/// Shared method between `on` and `manualOn`
@discardableResult
private func on(_ event: String, delegated: Delegated<Message, Void>) -> Int {
let ref = bindingRef
self.bindingRef = ref + 1
self.syncBindingsDel.append(Binding(event: event, ref: ref, callback: delegated))
return ref
}
/// Unsubscribes from a channel event. If a `ref` is given, only the exact
/// listener will be removed. Else all listeners for the `event` will be
/// removed.
///
/// Example:
///
/// let channel = socket.channel("topic")
/// let ref1 = channel.on("event") { _ in print("ref1 event" }
/// let ref2 = channel.on("event") { _ in print("ref2 event" }
/// let ref3 = channel.on("other_event") { _ in print("ref3 other" }
/// let ref4 = channel.on("other_event") { _ in print("ref4 other" }
/// channel.off("event", ref1)
/// channel.off("other_event")
///
/// After this, only "ref2 event" will be printed if the channel receives
/// "event" and nothing is printed if the channel receives "other_event".
///
/// - parameter event: Event to unsubscribe from
/// - paramter ref: Ref counter returned when subscribing. Can be omitted
public func off(_ event: String, ref: Int? = nil) {
self.syncBindingsDel.removeAll { (bind) -> Bool in
bind.event == event && (ref == nil || ref == bind.ref)
}
}
/// Push a payload to the Channel
///
/// Example:
///
/// channel
/// .push("event", payload: ["message": "hello")
/// .receive("ok") { _ in { print("message sent") }
///
/// - parameter event: Event to push
/// - parameter payload: Payload to push
/// - parameter timeout: Optional timeout
@discardableResult
public func push(_ event: String,
payload: Payload,
timeout: TimeInterval = Defaults.timeoutInterval) -> Push {
guard joinedOnce else { fatalError("Tried to push \(event) to \(self.topic) before joining. Use channel.join() before pushing events") }
let pushEvent = Push(channel: self,
event: event,
payload: payload,
timeout: timeout)
if canPush {
pushEvent.send()
} else {
pushEvent.startTimeout()
pushBuffer.append(pushEvent)
}
return pushEvent
}
/// Leaves the channel
///
/// Unsubscribes from server events, and instructs channel to terminate on
/// server
///
/// Triggers onClose() hooks
///
/// To receive leave acknowledgements, use the a `receive`
/// hook to bind to the server ack, ie:
///
/// Example:
////
/// channel.leave().receive("ok") { _ in { print("left") }
///
/// - parameter timeout: Optional timeout
/// - return: Push that can add receive hooks
@discardableResult
public func leave(timeout: TimeInterval = Defaults.timeoutInterval) -> Push {
// If attempting a rejoin during a leave, then reset, cancelling the rejoin
self.rejoinTimer.reset()
// Now set the state to leaving
self.state = .leaving
/// Delegated callback for a successful or a failed channel leave
var onCloseDelegate = Delegated<Message, Void>()
onCloseDelegate.delegate(to: self) { (self, message) in
self.socket?.logItems("channel", "leave \(self.topic)")
// Triggers onClose() hooks
self.trigger(event: ChannelEvent.close, payload: ["reason": "leave"])
}
// Push event to send to the server
let leavePush = Push(channel: self,
event: ChannelEvent.leave,
timeout: timeout)
// Perform the same behavior if successfully left the channel
// or if sending the event timed out
leavePush
.receive("ok", delegated: onCloseDelegate)
.receive("timeout", delegated: onCloseDelegate)
leavePush.send()
// If the Channel cannot send push events, trigger a success locally
if !canPush { leavePush.trigger("ok", payload: [:]) }
// Return the push so it can be bound to
return leavePush
}
/// Overridable message hook. Receives all events for specialized message
/// handling before dispatching to the channel callbacks.
///
/// - parameter event: The event the message was for
/// - parameter payload: The payload for the message
/// - parameter ref: The reference of the message
/// - return: Must return the payload, modified or unmodified
public func onMessage(callback: @escaping (Message) -> Message) {
self.onMessage = callback
}
//----------------------------------------------------------------------
// MARK: - Internal
//----------------------------------------------------------------------
/// Checks if an event received by the Socket belongs to this Channel
func isMember(_ message: Message) -> Bool {
// Return false if the message's topic does not match the Channel's topic
guard message.topic == self.topic else { return false }
guard
let safeJoinRef = message.joinRef,
safeJoinRef != self.joinRef,
ChannelEvent.isLifecyleEvent(message.event)
else { return true }
self.socket?.logItems("channel", "dropping outdated message", message.topic, message.event, message.rawPayload, safeJoinRef)
return false
}
/// Sends the payload to join the Channel
func sendJoin(_ timeout: TimeInterval) {
self.state = ChannelState.joining
self.joinPush.resend(timeout)
}
/// Rejoins the channel
func rejoin(_ timeout: TimeInterval? = nil) {
// Do not attempt to rejoin if the channel is in the process of leaving
guard !self.isLeaving else { return }
// Leave potentially duplicate channels
self.socket?.leaveOpenTopic(topic: self.topic)
// Send the joinPush
self.sendJoin(timeout ?? self.timeout)
}
/// Triggers an event to the correct event bindings created by
/// `channel.on("event")`.
///
/// - parameter message: Message to pass to the event bindings
func trigger(_ message: Message) {
let handledMessage = self.onMessage(message)
self.syncBindingsDel.forEach { binding in
if binding.event == message.event {
binding.callback.call(handledMessage)
}
}
}
/// Triggers an event to the correct event bindings created by
//// `channel.on("event")`.
///
/// - parameter event: Event to trigger
/// - parameter payload: Payload of the event
/// - parameter ref: Ref of the event. Defaults to empty
/// - parameter joinRef: Ref of the join event. Defaults to nil
func trigger(event: String,
payload: Payload = [:],
ref: String = "",
joinRef: String? = nil) {
let message = Message(ref: ref,
topic: self.topic,
event: event,
payload: payload,
joinRef: joinRef ?? self.joinRef)
self.trigger(message)
}
/// - parameter ref: The ref of the event push
/// - return: The event name of the reply
func replyEventName(_ ref: String) -> String {
return "chan_reply_\(ref)"
}
/// The Ref send during the join message.
var joinRef: String? {
return self.joinPush.ref
}
/// - return: True if the Channel can push messages, meaning the socket
/// is connected and the channel is joined
var canPush: Bool {
return self.socket?.isConnected == true && self.isJoined
}
}
//----------------------------------------------------------------------
// MARK: - Public API
//----------------------------------------------------------------------
extension Channel {
/// - return: True if the Channel has been closed
public var isClosed: Bool {
return state == .closed
}
/// - return: True if the Channel experienced an error
public var isErrored: Bool {
return state == .errored
}
/// - return: True if the channel has joined
public var isJoined: Bool {
return state == .joined
}
/// - return: True if the channel has requested to join
public var isJoining: Bool {
return state == .joining
}
/// - return: True if the channel has requested to leave
public var isLeaving: Bool {
return state == .leaving
}
}
================================================
FILE: Sources/SwiftPhoenixClient/Defaults.swift
================================================
// Copyright (c) 2021 David Stump <david@davidstump.net>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
/// A collection of default values and behaviors used across the Client
public class Defaults {
/// Default timeout when sending messages
public static let timeoutInterval: TimeInterval = 10.0
/// Default interval to send heartbeats on
public static let heartbeatInterval: TimeInterval = 30.0
/// Default maximum amount of time which the system may delay heartbeat events in order to minimize power usage
public static let heartbeatLeeway: DispatchTimeInterval = .milliseconds(10)
/// Default reconnect algorithm for the socket
public static let reconnectSteppedBackOff: (Int) -> TimeInterval = { tries in
return tries > 9 ? 5.0 : [0.01, 0.05, 0.1, 0.15, 0.2, 0.25, 0.5, 1.0, 2.0][tries - 1]
}
/** Default rejoin algorithm for individual channels */
public static let rejoinSteppedBackOff: (Int) -> TimeInterval = { tries in
return tries > 3 ? 10 : [1, 2, 5][tries - 1]
}
public static let vsn = "2.0.0"
/// Default encode function, utilizing JSONSerialization.data
public static let encode: (Any) -> Data = { json in
return try! JSONSerialization
.data(withJSONObject: json,
options: JSONSerialization.WritingOptions())
}
/// Default decode function, utilizing JSONSerialization.jsonObject
public static let decode: (Data) -> Any? = { data in
guard
let json = try? JSONSerialization
.jsonObject(with: data,
options: JSONSerialization.ReadingOptions())
else { return nil }
return json
}
public static let heartbeatQueue: DispatchQueue
= DispatchQueue(label: "com.phoenix.socket.heartbeat")
}
/// Represents the multiple states that a Channel can be in
/// throughout it's lifecycle.
public enum ChannelState: String {
case closed = "closed"
case errored = "errored"
case joined = "joined"
case joining = "joining"
case leaving = "leaving"
}
/// Represents the different events that can be sent through
/// a channel regarding a Channel's lifecycle.
public struct ChannelEvent {
public static let heartbeat = "heartbeat"
public static let join = "phx_join"
public static let leave = "phx_leave"
public static let reply = "phx_reply"
public static let error = "phx_error"
public static let close = "phx_close"
static func isLifecyleEvent(_ event: String) -> Bool {
switch event {
case join, leave, reply, error, close: return true
default: return false
}
}
}
================================================
FILE: Sources/SwiftPhoenixClient/Delegated.swift
================================================
// Copyright (c) 2021 David Stump <david@davidstump.net>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
/// Provides a memory-safe way of passing callbacks around while not creating
/// retain cycles. This file was copied from https://github.com/dreymonde/Delegated
/// instead of added as a dependency to reduce the number of packages that
/// ship with SwiftPhoenixClient
public struct Delegated<Input, Output> {
private(set) var callback: ((Input) -> Output?)?
public init() { }
public mutating func delegate<Target : AnyObject>(to target: Target,
with callback: @escaping (Target, Input) -> Output) {
self.callback = { [weak target] input in
guard let target = target else {
return nil
}
return callback(target, input)
}
}
public func call(_ input: Input) -> Output? {
return self.callback?(input)
}
public var isDelegateSet: Bool {
return callback != nil
}
}
extension Delegated {
public mutating func stronglyDelegate<Target : AnyObject>(to target: Target,
with callback: @escaping (Target, Input) -> Output) {
self.callback = { input in
return callback(target, input)
}
}
public mutating func manuallyDelegate(with callback: @escaping (Input) -> Output) {
self.callback = callback
}
public mutating func removeDelegate() {
self.callback = nil
}
}
extension Delegated where Input == Void {
public mutating func delegate<Target : AnyObject>(to target: Target,
with callback: @escaping (Target) -> Output) {
self.delegate(to: target, with: { target, voidInput in callback(target) })
}
public mutating func stronglyDelegate<Target : AnyObject>(to target: Target,
with callback: @escaping (Target) -> Output) {
self.stronglyDelegate(to: target, with: { target, voidInput in callback(target) })
}
}
extension Delegated where Input == Void {
public func call() -> Output? {
return self.call(())
}
}
extension Delegated where Output == Void {
public func call(_ input: Input) {
self.callback?(input)
}
}
extension Delegated where Input == Void, Output == Void {
public func call() {
self.call(())
}
}
================================================
FILE: Sources/SwiftPhoenixClient/HeartbeatTimer.swift
================================================
// Copyright (c) 2021 David Stump <david@davidstump.net>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
/**
Heartbeat Timer class which manages the lifecycle of the underlying
timer which triggers when a heartbeat should be fired. This heartbeat
runs on it's own Queue so that it does not interfere with the main
queue but guarantees thread safety.
*/
class HeartbeatTimer {
//----------------------------------------------------------------------
// MARK: - Dependencies
//----------------------------------------------------------------------
// The interval to wait before firing the Timer
let timeInterval: TimeInterval
/// The maximum amount of time which the system may delay the delivery of the timer events
let leeway: DispatchTimeInterval
// The DispatchQueue to schedule the timers on
let queue: DispatchQueue
// UUID which specifies the Timer instance. Verifies that timers are different
let uuid: String = UUID().uuidString
//----------------------------------------------------------------------
// MARK: - Properties
//----------------------------------------------------------------------
// The underlying, cancelable, resettable, timer.
private var temporaryTimer: DispatchSourceTimer? = nil
// The event handler that is called by the timer when it fires.
private var temporaryEventHandler: (() -> Void)? = nil
/**
Create a new HeartbeatTimer
- Parameters:
- timeInterval: Interval to fire the timer. Repeats
- queue: Queue to schedule the timer on
- leeway: The maximum amount of time which the system may delay the delivery of the timer events
*/
init(timeInterval: TimeInterval, queue: DispatchQueue = Defaults.heartbeatQueue, leeway: DispatchTimeInterval = Defaults.heartbeatLeeway) {
self.timeInterval = timeInterval
self.queue = queue
self.leeway = leeway
}
/**
Create a new HeartbeatTimer
- Parameter timeInterval: Interval to fire the timer. Repeats
*/
convenience init(timeInterval: TimeInterval) {
self.init(timeInterval: timeInterval, queue: Defaults.heartbeatQueue)
}
func start(eventHandler: @escaping () -> Void) {
queue.sync { [weak self] in
guard let self = self else { return }
// Create a new DispatchSourceTimer, passing the event handler
let timer = DispatchSource.makeTimerSource(flags: [], queue: queue)
timer.setEventHandler(handler: eventHandler)
// Schedule the timer to first fire in `timeInterval` and then
// repeat every `timeInterval`
timer.schedule(deadline: DispatchTime.now() + self.timeInterval,
repeating: self.timeInterval,
leeway: self.leeway)
// Start the timer
timer.resume()
self.temporaryEventHandler = eventHandler
self.temporaryTimer = timer
}
}
func stop() {
// Must be queued synchronously to prevent threading issues.
queue.sync { [weak self] in
guard let self = self else { return }
// DispatchSourceTimer will automatically cancel when released
temporaryTimer = nil
temporaryEventHandler = nil
}
}
/**
True if the Timer exists and has not been cancelled. False otherwise
*/
var isValid: Bool {
guard let timer = self.temporaryTimer else { return false }
return !timer.isCancelled
}
/**
Calls the Timer's event handler immediately. This method
is primarily used in tests (not ideal)
*/
func fire() {
guard isValid else { return }
self.temporaryEventHandler?()
}
}
extension HeartbeatTimer: Equatable {
static func == (lhs: HeartbeatTimer, rhs: HeartbeatTimer) -> Bool {
return lhs.uuid == rhs.uuid
}
}
================================================
FILE: Sources/SwiftPhoenixClient/Message.swift
================================================
// Copyright (c) 2021 David Stump <david@davidstump.net>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
/// Data that is received from the Server.
public class Message {
/// Reference number. Empty if missing
public let ref: String
/// Join Reference number
internal let joinRef: String?
/// Message topic
public let topic: String
/// Message event
public let event: String
/// The raw payload from the Message, including a nested response from
/// phx_reply events. It is recommended to use `payload` instead.
internal let rawPayload: Payload
/// Message payload
public var payload: Payload {
guard let response = rawPayload["response"] as? Payload
else { return rawPayload }
return response
}
/// Convenience accessor. Equivalent to getting the status as such:
/// ```swift
/// message.payload["status"]
/// ```
public var status: String? {
return rawPayload["status"] as? String
}
init(ref: String = "",
topic: String = "",
event: String = "",
payload: Payload = [:],
joinRef: String? = nil) {
self.ref = ref
self.topic = topic
self.event = event
self.rawPayload = payload
self.joinRef = joinRef
}
init?(json: [Any?]) {
guard json.count > 4 else { return nil }
self.joinRef = json[0] as? String
self.ref = json[1] as? String ?? ""
if
let topic = json[2] as? String,
let event = json[3] as? String,
let payload = json[4] as? Payload {
self.topic = topic
self.event = event
self.rawPayload = payload
} else {
return nil
}
}
}
================================================
FILE: Sources/SwiftPhoenixClient/PhoenixTransport.swift
================================================
// Copyright (c) 2021 David Stump <david@davidstump.net>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
//----------------------------------------------------------------------
// MARK: - Transport Protocol
//----------------------------------------------------------------------
/**
Defines a `Socket`'s Transport layer.
*/
// sourcery: AutoMockable
public protocol PhoenixTransport {
/// The current `ReadyState` of the `Transport` layer
var readyState: PhoenixTransportReadyState { get }
/// Delegate for the `Transport` layer
var delegate: PhoenixTransportDelegate? { get set }
/**
Connect to the server
- Parameters:
- headers: Headers to include in the URLRequests when opening the Websocket connection. Can be empty [:]
*/
func connect(with headers: [String: Any])
/**
Disconnect from the server.
- Parameters:
- code: Status code as defined by <ahref="http://tools.ietf.org/html/rfc6455#section-7.4">Section 7.4 of RFC 6455</a>.
- reason: Reason why the connection is closing. Optional.
*/
func disconnect(code: Int, reason: String?)
/**
Sends a message to the server.
- Parameter data: Data to send.
*/
func send(data: Data)
}
//----------------------------------------------------------------------
// MARK: - Transport Delegate Protocol
//----------------------------------------------------------------------
/**
Delegate to receive notifications of events that occur in the `Transport` layer
*/
public protocol PhoenixTransportDelegate {
/**
Notified when the `Transport` opens.
- Parameter response: Response from the server indicating that the WebSocket handshake was successful and the connection has been upgraded to webSockets
*/
func onOpen(response: URLResponse?)
/**
Notified when the `Transport` receives an error.
- Parameter error: Client-side error from the underlying `Transport` implementation
- Parameter response: Response from the server, if any, that occurred with the Error
*/
func onError(error: Error, response: URLResponse?)
/**
Notified when the `Transport` receives a message from the server.
- Parameter message: Message received from the server
*/
func onMessage(message: String)
/**
Notified when the `Transport` closes.
- Parameter code: Code that was sent when the `Transport` closed
- Parameter reason: A concise human-readable prose explanation for the closure
*/
func onClose(code: Int, reason: String?)
}
//----------------------------------------------------------------------
// MARK: - Transport Ready State Enum
//----------------------------------------------------------------------
/**
Available `ReadyState`s of a `Transport` layer.
*/
public enum PhoenixTransportReadyState {
/// The `Transport` is opening a connection to the server.
case connecting
/// The `Transport` is connected to the server.
case open
/// The `Transport` is closing the connection to the server.
case closing
/// The `Transport` has disconnected from the server.
case closed
}
//----------------------------------------------------------------------
// MARK: - Default Websocket Transport Implementation
//----------------------------------------------------------------------
/**
A `Transport` implementation that relies on URLSession's native WebSocket
implementation.
This implementation ships default with SwiftPhoenixClient however
SwiftPhoenixClient supports earlier OS versions using one of the submodule
`Transport` implementations. Or you can create your own implementation using
your own WebSocket library or implementation.
*/
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
open class URLSessionTransport: NSObject, PhoenixTransport, URLSessionWebSocketDelegate {
/// The URL to connect to
internal let url: URL
/// The URLSession configuration
internal let configuration: URLSessionConfiguration
/// The underling URLSession. Assigned during `connect()`
private var session: URLSession? = nil
/// The ongoing task. Assigned during `connect()`
private var task: URLSessionWebSocketTask? = nil
/// Holds the current receive task
private var receiveMessageTask: Task<Void, Never>? {
didSet {
oldValue?.cancel()
}
}
/**
Initializes a `Transport` layer built using URLSession's WebSocket
Example:
```swift
let url = URL("wss://example.com/socket")
let transport: Transport = URLSessionTransport(url: url)
```
Using a custom `URLSessionConfiguration`
```swift
let url = URL("wss://example.com/socket")
let configuration = URLSessionConfiguration.default
let transport: Transport = URLSessionTransport(url: url, configuration: configuration)
```
- parameter url: URL to connect to
- parameter configuration: Provide your own URLSessionConfiguration. Uses `.default` if none provided
*/
public init(url: URL, configuration: URLSessionConfiguration = .default) {
// URLSession requires that the endpoint be "wss" instead of "https".
let endpoint = url.absoluteString
let wsEndpoint = endpoint
.replacingOccurrences(of: "http://", with: "ws://")
.replacingOccurrences(of: "https://", with: "wss://")
// Force unwrapping should be safe here since a valid URL came in and we just
// replaced the protocol.
self.url = URL(string: wsEndpoint)!
self.configuration = configuration
super.init()
}
deinit {
self.delegate = nil
receiveMessageTask?.cancel()
}
// MARK: - Transport
public var readyState: PhoenixTransportReadyState = .closed
public var delegate: PhoenixTransportDelegate? = nil
public func connect(with headers: [String : Any]) {
// Set the transport state as connecting
self.readyState = .connecting
// Create the session and websocket task
self.session = URLSession(configuration: self.configuration, delegate: self, delegateQueue: nil)
var request = URLRequest(url: url)
headers.forEach { (key: String, value: Any) in
guard let value = value as? String else { return }
request.addValue(value, forHTTPHeaderField: key)
}
self.task = self.session?.webSocketTask(with: request)
// Start the task
self.task?.resume()
}
open func disconnect(code: Int, reason: String?) {
/*
TODO:
1. Provide a "strict" mode that fails if an invalid close code is given
2. If strict mode is disabled, default to CloseCode.invalid
3. Provide default .normalClosure function
*/
guard let closeCode = URLSessionWebSocketTask.CloseCode.init(rawValue: code) else {
fatalError("Could not create a CloseCode with invalid code: [\(code)].")
}
self.readyState = .closing
self.task?.cancel(with: closeCode, reason: reason?.data(using: .utf8))
self.session?.finishTasksAndInvalidate()
receiveMessageTask?.cancel()
}
open func send(data: Data) {
self.task?.send(.string(String(data: data, encoding: .utf8)!)) { (error) in
// TODO: What is the behavior when an error occurs?
}
}
// MARK: - URLSessionWebSocketDelegate
open func urlSession(_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didOpenWithProtocol protocol: String?) {
// The Websocket is connected. Set Transport state to open and inform delegate
self.readyState = .open
self.delegate?.onOpen(response: webSocketTask.response)
// Start receiving messages
self.receive()
}
open func urlSession(_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
reason: Data?) {
// A close frame was received from the server.
self.readyState = .closed
self.delegate?.onClose(code: closeCode.rawValue, reason: reason.flatMap { String(data: $0, encoding: .utf8) })
}
open func urlSession(_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?) {
// The task has terminated. Inform the delegate that the transport has closed abnormally
// if this was caused by an error.
guard let err = error else { return }
self.abnormalErrorReceived(err, response: task.response)
}
// MARK: - Private
private func receive() {
receiveMessageTask = Task { [weak self] in
guard let self else { return }
do {
let message = try await task?.receive()
switch message {
case .data:
print("Data received. This method is unsupported by the Client")
case .string(let text):
delegate?.onMessage(message: text)
default:
fatalError("Nil message received.")
}
// Since `.receive()` is only good for a single message, it must
// be called again after a message is received in order to
// received the next message.
receive()
} catch {
print("Error when receiving \(error)")
abnormalErrorReceived(error, response: nil)
}
}
}
private func abnormalErrorReceived(_ error: Error, response: URLResponse?) {
// Set the state of the Transport to closed
self.readyState = .closed
// Inform the Transport's delegate that an error occurred.
self.delegate?.onError(error: error, response: response)
// An abnormal error is results in an abnormal closure, such as internet getting dropped
// so inform the delegate that the Transport has closed abnormally. This will kick off
// the reconnect logic.
self.delegate?.onClose(code: Socket.CloseCode.abnormal.rawValue, reason: error.localizedDescription)
}
}
================================================
FILE: Sources/SwiftPhoenixClient/Presence.swift
================================================
// Copyright (c) 2021 David Stump <david@davidstump.net>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
/// The Presence object provides features for syncing presence information from
/// the server with the client and handling presences joining and leaving.
///
/// ## Syncing state from the server
///
/// To sync presence state from the server, first instantiate an object and pass
/// your channel in to track lifecycle events:
///
/// let channel = socket.channel("some:topic")
/// let presence = Presence(channel)
///
/// If you have custom syncing state events, you can configure the `Presence`
/// object to use those instead.
///
/// let options = Options(events: [.state: "my_state", .diff: "my_diff"])
/// let presence = Presence(channel, opts: options)
///
/// Next, use the presence.onSync callback to react to state changes from the
/// server. For example, to render the list of users every time the list
/// changes, you could write:
///
/// presence.onSync { renderUsers(presence.list()) }
///
/// ## Listing Presences
///
/// presence.list is used to return a list of presence information based on the
/// local state of metadata. By default, all presence metadata is returned, but
/// a listBy function can be supplied to allow the client to select which
/// metadata to use for a given presence. For example, you may have a user
/// online from different devices with a metadata status of "online", but they
/// have set themselves to "away" on another device. In this case, the app may
/// choose to use the "away" status for what appears on the UI. The example
/// below defines a listBy function which prioritizes the first metadata which
/// was registered for each user. This could be the first tab they opened, or
/// the first device they came online from:
///
/// let listBy: (String, Presence.Map) -> Presence.Meta = { id, pres in
/// let first = pres["metas"]!.first!
/// first["count"] = pres["metas"]!.count
/// first["id"] = id
/// return first
/// }
/// let onlineUsers = presence.list(by: listBy)
///
/// (NOTE: The underlying behavior is a `map` on the `presence.state`. You are
/// mapping the `state` dictionary into whatever datastructure suites your needs)
///
/// ## Handling individual presence join and leave events
///
/// The presence.onJoin and presence.onLeave callbacks can be used to react to
/// individual presences joining and leaving the app. For example:
///
/// let presence = Presence(channel)
/// presence.onJoin { [weak self] (key, current, newPres) in
/// if let cur = current {
/// print("user additional presence", cur)
/// } else {
/// print("user entered for the first time", newPres)
/// }
/// }
///
/// presence.onLeave { [weak self] (key, current, leftPres) in
/// if current["metas"]?.isEmpty == true {
/// print("user has left from all devices", leftPres)
/// } else {
/// print("user left from a device", current)
/// }
/// }
///
/// presence.onSync { renderUsers(presence.list()) }
public final class Presence {
//----------------------------------------------------------------------
// MARK: - Enums and Structs
//----------------------------------------------------------------------
/// Custom options that can be provided when creating Presence
///
/// ### Example:
///
/// let options = Options(events: [.state: "my_state", .diff: "my_diff"])
/// let presence = Presence(channel, opts: options)
public struct Options {
let events: [Events: String]
/// Default set of Options used when creating Presence. Uses the
/// phoenix events "presence_state" and "presence_diff"
static public let defaults
= Options(events: [.state: "presence_state",
.diff: "presence_diff"])
public init(events: [Events: String]) {
self.events = events
}
}
/// Presense Events
public enum Events: String {
case state = "state"
case diff = "diff"
}
//----------------------------------------------------------------------
// MARK: - Typaliases
//----------------------------------------------------------------------
/// Meta details of a Presence. Just a dictionary of properties
public typealias Meta = [String: Any]
/// A mapping of a String to an array of Metas. e.g. {"metas": [{id: 1}]}
public typealias Map = [String: [Meta]]
/// A mapping of a Presence state to a mapping of Metas
public typealias State = [String: Map]
// Diff has keys "joins" and "leaves", pointing to a Presence.State each
// containing the users that joined and left.
public typealias Diff = [String: State]
/// Closure signature of OnJoin callbacks
public typealias OnJoin = (_ key: String, _ current: Map?, _ new: Map) -> Void
/// Closure signature for OnLeave callbacks
public typealias OnLeave = (_ key: String, _ current: Map, _ left: Map) -> Void
//// Closure signature for OnSync callbacks
public typealias OnSync = () -> Void
/// Collection of callbacks with default values
struct Caller {
var onJoin: OnJoin = {_,_,_ in }
var onLeave: OnLeave = {_,_,_ in }
var onSync: OnSync = {}
}
//----------------------------------------------------------------------
// MARK: - Properties
//----------------------------------------------------------------------
/// The channel the Presence belongs to
weak var channel: Channel?
/// Caller to callback hooks
var caller: Caller
/// The state of the Presence
private(set) public var state: State
/// Pending `join` and `leave` diffs that need to be synced
private(set) public var pendingDiffs: [Diff]
/// The channel's joinRef, set when state events occur
private(set) public var joinRef: String?
public var isPendingSyncState: Bool {
guard let safeJoinRef = self.joinRef else { return true }
return safeJoinRef != self.channel?.joinRef
}
/// Callback to be informed of joins
public var onJoin: OnJoin {
get { return caller.onJoin }
set { caller.onJoin = newValue }
}
/// Set the OnJoin callback
public func onJoin(_ callback: @escaping OnJoin) {
self.onJoin = callback
}
/// Callback to be informed of leaves
public var onLeave: OnLeave {
get { return caller.onLeave }
set { caller.onLeave = newValue }
}
/// Set the OnLeave callback
public func onLeave(_ callback: @escaping OnLeave) {
self.onLeave = callback
}
/// Callback to be informed of synces
public var onSync: OnSync {
get { return caller.onSync }
set { caller.onSync = newValue }
}
/// Set the OnSync callback
public func onSync(_ callback: @escaping OnSync) {
self.onSync = callback
}
public init(channel: Channel, opts: Options = Options.defaults) {
self.state = [:]
self.pendingDiffs = []
self.channel = channel
self.joinRef = nil
self.caller = Caller()
guard // Do not subscribe to events if they were not provided
let stateEvent = opts.events[.state],
let diffEvent = opts.events[.diff] else { return }
self.channel?.delegateOn(stateEvent, to: self) { (self, message) in
guard let newState = message.rawPayload as? State else { return }
self.joinRef = self.channel?.joinRef
self.state = Presence.syncState(self.state,
newState: newState,
onJoin: self.caller.onJoin,
onLeave: self.caller.onLeave)
self.pendingDiffs.forEach({ (diff) in
self.state = Presence.syncDiff(self.state,
diff: diff,
onJoin: self.caller.onJoin,
onLeave: self.caller.onLeave)
})
self.pendingDiffs = []
self.caller.onSync()
}
self.channel?.delegateOn(diffEvent, to: self) { (self, message) in
guard let diff = message.rawPayload as? Diff else { return }
if self.isPendingSyncState {
self.pendingDiffs.append(diff)
} else {
self.state = Presence.syncDiff(self.state,
diff: diff,
onJoin: self.caller.onJoin,
onLeave: self.caller.onLeave)
self.caller.onSync()
}
}
}
/// Returns the array of presences, with deault selected metadata.
public func list() -> [Map] {
return self.list(by: { _, pres in pres })
}
/// Returns the array of presences, with selected metadata
public func list<T>(by transformer: (String, Map) -> T) -> [T] {
return Presence.listBy(self.state, transformer: transformer)
}
/// Filter the Presence state with a given function
public func filter(by filter: ((String, Map) -> Bool)?) -> State {
return Presence.filter(self.state, by: filter)
}
//----------------------------------------------------------------------
// MARK: - Static
//----------------------------------------------------------------------
// Used to sync the list of presences on the server
// with the client's state. An optional `onJoin` and `onLeave` callback can
// be provided to react to changes in the client's local presences across
// disconnects and reconnects with the server.
//
// - returns: Presence.State
@discardableResult
public static func syncState(_ currentState: State,
newState: State,
onJoin: OnJoin = {_,_,_ in },
onLeave: OnLeave = {_,_,_ in }) -> State {
let state = currentState
var leaves: Presence.State = [:]
var joins: Presence.State = [:]
state.forEach { (key, presence) in
if newState[key] == nil {
leaves[key] = presence
}
}
newState.forEach { (key, newPresence) in
if let currentPresence = state[key] {
let newRefs = newPresence["metas"]!.map({ $0["phx_ref"] as! String })
let curRefs = currentPresence["metas"]!.map({ $0["phx_ref"] as! String })
let joinedMetas = newPresence["metas"]!.filter({ (meta: Meta) -> Bool in
!curRefs.contains { $0 == meta["phx_ref"] as! String }
})
let leftMetas = currentPresence["metas"]!.filter({ (meta: Meta) -> Bool in
!newRefs.contains { $0 == meta["phx_ref"] as! String }
})
if joinedMetas.count > 0 {
joins[key] = newPresence
joins[key]!["metas"] = joinedMetas
}
if leftMetas.count > 0 {
leaves[key] = currentPresence
leaves[key]!["metas"] = leftMetas
}
} else {
joins[key] = newPresence
}
}
return Presence.syncDiff(state,
diff: ["joins": joins, "leaves": leaves],
onJoin: onJoin,
onLeave: onLeave)
}
// Used to sync a diff of presence join and leave
// events from the server, as they happen. Like `syncState`, `syncDiff`
// accepts optional `onJoin` and `onLeave` callbacks to react to a user
// joining or leaving from a device.
//
// - returns: Presence.State
@discardableResult
public static func syncDiff(_ currentState: State,
diff: Diff,
onJoin: OnJoin = {_,_,_ in },
onLeave: OnLeave = {_,_,_ in }) -> State {
var state = currentState
diff["joins"]?.forEach { (key, newPresence) in
let currentPresence = state[key]
state[key] = newPresence
if let curPresence = currentPresence {
let joinedRefs = state[key]!["metas"]!.map({ $0["phx_ref"] as! String })
let curMetas = curPresence["metas"]!.filter { (meta: Meta) -> Bool in
!joinedRefs.contains { $0 == meta["phx_ref"] as! String }
}
state[key]!["metas"]!.insert(contentsOf: curMetas, at: 0)
}
onJoin(key, currentPresence, newPresence)
}
diff["leaves"]?.forEach({ (key, leftPresence) in
guard var curPresence = state[key] else { return }
let refsToRemove = leftPresence["metas"]!.map { $0["phx_ref"] as! String }
let keepMetas = curPresence["metas"]!.filter { (meta: Meta) -> Bool in
!refsToRemove.contains { $0 == meta["phx_ref"] as! String }
}
curPresence["metas"] = keepMetas
onLeave(key, curPresence, leftPresence)
if keepMetas.count > 0 {
state[key]!["metas"] = keepMetas
} else {
state.removeValue(forKey: key)
}
})
return state
}
public static func filter(_ presences: State,
by filter: ((String, Map) -> Bool)?) -> State {
let safeFilter = filter ?? { key, pres in true }
return presences.filter(safeFilter)
}
public static func listBy<T>(_ presences: State,
transformer: (String, Map) -> T) -> [T] {
return presences.map(transformer)
}
}
================================================
FILE: Sources/SwiftPhoenixClient/Push.swift
================================================
// Copyright (c) 2021 David Stump <david@davidstump.net>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
/// Represnts pushing data to a `Channel` through the `Socket`
public class Push {
/// The channel sending the Push
public weak var channel: Channel?
/// The event, for example `phx_join`
public let event: String
/// The payload, for example ["user_id": "abc123"]
public var payload: Payload
/// The push timeout. Default is 10.0 seconds
public var timeout: TimeInterval
/// The server's response to the Push
var receivedMessage: Message?
/// Timer which triggers a timeout event
var timeoutTimer: TimerQueue
/// WorkItem to be performed when the timeout timer fires
var timeoutWorkItem: DispatchWorkItem?
/// Hooks into a Push. Where .receive("ok", callback(Payload)) are stored
var receiveHooks: [String: [Delegated<Message, Void>]]
/// True if the Push has been sent
var sent: Bool
/// The reference ID of the Push
var ref: String?
/// The event that is associated with the reference ID of the Push
var refEvent: String?
/// Initializes a Push
///
/// - parameter channel: The Channel
/// - parameter event: The event, for example ChannelEvent.join
/// - parameter payload: Optional. The Payload to send, e.g. ["user_id": "abc123"]
/// - parameter timeout: Optional. The push timeout. Default is 10.0s
init(channel: Channel,
event: String,
payload: Payload = [:],
timeout: TimeInterval = Defaults.timeoutInterval) {
self.channel = channel
self.event = event
self.payload = payload
self.timeout = timeout
self.receivedMessage = nil
self.timeoutTimer = TimerQueue.main
self.receiveHooks = [:]
self.sent = false
self.ref = nil
}
/// Resets and sends the Push
/// - parameter timeout: Optional. The push timeout. Default is 10.0s
public func resend(_ timeout: TimeInterval = Defaults.timeoutInterval) {
self.timeout = timeout
self.reset()
self.send()
}
/// Sends the Push. If it has already timed out, then the call will
/// be ignored and return early. Use `resend` in this case.
public func send() {
guard !hasReceived(status: "timeout") else { return }
self.startTimeout()
self.sent = true
self.channel?.socket?.push(
topic: channel?.topic ?? "",
event: self.event,
payload: self.payload,
ref: self.ref,
joinRef: channel?.joinRef
)
}
/// Receive a specific event when sending an Outbound message. Subscribing
/// to status events with this method does not guarantees no retain cycles.
/// You should pass `weak self` in the capture list of the callback. You
/// can call `.delegateReceive(status:, to:, callback:) and the library will
/// handle it for you.
///
/// Example:
///
/// channel
/// .send(event:"custom", payload: ["body": "example"])
/// .receive("error") { [weak self] payload in
/// print("Error: ", payload)
/// }
///
/// - parameter status: Status to receive
/// - parameter callback: Callback to fire when the status is recevied
@discardableResult
public func receive(_ status: String,
callback: @escaping ((Message) -> ())) -> Push {
var delegated = Delegated<Message, Void>()
delegated.manuallyDelegate(with: callback)
return self.receive(status, delegated: delegated)
}
/// Receive a specific event when sending an Outbound message. Automatically
/// prevents retain cycles. See `manualReceive(status:, callback:)` if you
/// want to handle this yourself.
///
/// Example:
///
/// channel
/// .send(event:"custom", payload: ["body": "example"])
/// .delegateReceive("error", to: self) { payload in
/// print("Error: ", payload)
/// }
///
/// - parameter status: Status to receive
/// - parameter owner: The class that is calling .receive. Usually `self`
/// - parameter callback: Callback to fire when the status is recevied
@discardableResult
public func delegateReceive<Target: AnyObject>(_ status: String,
to owner: Target,
callback: @escaping ((Target, Message) -> ())) -> Push {
var delegated = Delegated<Message, Void>()
delegated.delegate(to: owner, with: callback)
return self.receive(status, delegated: delegated)
}
/// Shared behavior between `receive` calls
@discardableResult
internal func receive(_ status: String, delegated: Delegated<Message, Void>) -> Push {
// If the message has already been received, pass it to the callback immediately
if hasReceived(status: status), let receivedMessage = self.receivedMessage {
delegated.call(receivedMessage)
}
if receiveHooks[status] == nil {
/// Create a new array of hooks if no previous hook is associated with status
receiveHooks[status] = [delegated]
} else {
/// A previous hook for this status already exists. Just append the new hook
receiveHooks[status]?.append(delegated)
}
return self
}
/// Resets the Push as it was after it was first tnitialized.
internal func reset() {
self.cancelRefEvent()
self.ref = nil
self.refEvent = nil
self.receivedMessage = nil
self.sent = false
}
/// Finds the receiveHook which needs to be informed of a status response
///
/// - parameter status: Status which was received, e.g. "ok", "error", "timeout"
/// - parameter response: Response that was received
private func matchReceive(_ status: String, message: Message) {
receiveHooks[status]?.forEach( { $0.call(message) } )
}
/// Reverses the result on channel.on(ChannelEvent, callback) that spawned the Push
private func cancelRefEvent() {
guard let refEvent = self.refEvent else { return }
self.channel?.off(refEvent)
}
/// Cancel any ongoing Timeout Timer
internal func cancelTimeout() {
self.timeoutWorkItem?.cancel()
self.timeoutWorkItem = nil
}
/// Starts the Timer which will trigger a timeout after a specific _timeout_
/// time, in milliseconds, is reached.
internal func startTimeout() {
// Cancel any existing timeout before starting a new one
if let safeWorkItem = timeoutWorkItem, !safeWorkItem.isCancelled {
self.cancelTimeout()
}
guard
let channel = channel,
let socket = channel.socket else { return }
let ref = socket.makeRef()
let refEvent = channel.replyEventName(ref)
self.ref = ref
self.refEvent = refEvent
/// If a response is received before the Timer triggers, cancel timer
/// and match the recevied event to it's corresponding hook
channel.delegateOn(refEvent, to: self) { (self, message) in
self.cancelRefEvent()
self.cancelTimeout()
self.receivedMessage = message
/// Check if there is event a status available
guard let status = message.status else { return }
self.matchReceive(status, message: message)
}
/// Setup and start the Timeout timer.
let workItem = DispatchWorkItem {
self.trigger("timeout", payload: [:])
}
self.timeoutWorkItem = workItem
self.timeoutTimer.queue(timeInterval: timeout, execute: workItem)
}
/// Checks if a status has already been received by the Push.
///
/// - parameter status: Status to check
/// - return: True if given status has been received by the Push.
internal func hasReceived(status: String) -> Bool {
return self.receivedMessage?.status == status
}
/// Triggers an event to be sent though the Channel
internal func trigger(_ status: String, payload: Payload) {
/// If there is no ref event, then there is nothing to trigger on the channel
guard let refEvent = self.refEvent else { return }
var mutPayload = payload
mutPayload["status"] = status
self.channel?.trigger(event: refEvent, payload: mutPayload)
}
}
================================================
FILE: Sources/SwiftPhoenixClient/Socket.swift
================================================
// Copyright (c) 2021 David Stump <david@davidstump.net>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
public enum SocketError: Error {
case abnormalClosureError
}
/// Alias for a JSON dictionary [String: Any]
public typealias Payload = [String: Any]
/// Alias for a function returning an optional JSON dictionary (`Payload?`)
public typealias PayloadClosure = () -> Payload?
/// Struct that gathers callbacks assigned to the Socket
struct StateChangeCallbacks {
let open: SynchronizedArray<(ref: String, callback: Delegated<URLResponse?, Void>)> = .init()
let close: SynchronizedArray<(ref: String, callback: Delegated<(Int, String?), Void>)> = .init()
let error: SynchronizedArray<(ref: String, callback: Delegated<(Error, URLResponse?), Void>)> = .init()
let message: SynchronizedArray<(ref: String, callback: Delegated<Message, Void>)> = .init()
}
/// ## Socket Connection
/// A single connection is established to the server and
/// channels are multiplexed over the connection.
/// Connect to the server using the `Socket` class:
///
/// ```swift
/// let socket = new Socket("/socket", paramsClosure: { ["userToken": "123" ] })
/// socket.connect()
/// ```
///
/// The `Socket` constructor takes the mount point of the socket,
/// the authentication params, as well as options that can be found in
/// the Socket docs, such as configuring the heartbeat.
public class Socket: PhoenixTransportDelegate {
//----------------------------------------------------------------------
// MARK: - Public Attributes
//----------------------------------------------------------------------
/// The string WebSocket endpoint (ie `"ws://example.com/socket"`,
/// `"wss://example.com"`, etc.) That was passed to the Socket during
/// initialization. The URL endpoint will be modified by the Socket to
/// include `"/websocket"` if missing.
public let endPoint: String
/// The fully qualified socket URL
public private(set) var endPointUrl: URL
/// Resolves to return the `paramsClosure` result at the time of calling.
/// If the `Socket` was created with static params, then those will be
/// returned every time.
public var params: Payload? {
return self.paramsClosure?()
}
/// The optional params closure used to get params when connecting. Must
/// be set when initializing the Socket.
public let paramsClosure: PayloadClosure?
/// The WebSocket transport. Default behavior is to provide a
/// URLSessionWebsocketTask. See README for alternatives.
private let transport: ((URL) -> PhoenixTransport)
/// Phoenix serializer version, defaults to "2.0.0"
public let vsn: String
/// Override to provide custom encoding of data before writing to the socket
public var encode: (Any) -> Data = Defaults.encode
/// Override to provide custom decoding of data read from the socket
public var decode: (Data) -> Any? = Defaults.decode
/// Timeout to use when opening connections
public var timeout: TimeInterval = Defaults.timeoutInterval
/// Custom headers to be added to the socket connection request
public var headers: [String : Any] = [:]
/// Interval between sending a heartbeat
public var heartbeatInterval: TimeInterval = Defaults.heartbeatInterval
/// The maximum amount of time which the system may delay heartbeats in order to optimize power usage
public var heartbeatLeeway: DispatchTimeInterval = Defaults.heartbeatLeeway
/// Interval between socket reconnect attempts, in seconds
public var reconnectAfter: (Int) -> TimeInterval = Defaults.reconnectSteppedBackOff
/// Interval between channel rejoin attempts, in seconds
public var rejoinAfter: (Int) -> TimeInterval = Defaults.rejoinSteppedBackOff
/// The optional function to receive logs
public var logger: ((String) -> Void)?
/// Disables heartbeats from being sent. Default is false.
public var skipHeartbeat: Bool = false
/// Enable/Disable SSL certificate validation. Default is false. This
/// must be set before calling `socket.connect()` in order to be applied
public var disableSSLCertValidation: Bool = false
#if os(Linux)
#else
/// Configure custom SSL validation logic, eg. SSL pinning. This
/// must be set before calling `socket.connect()` in order to apply.
// public var security: SSLTrustValidator?
/// Configure the encryption used by your client by setting the
/// allowed cipher suites supported by your server. This must be
/// set before calling `socket.connect()` in order to apply.
public var enabledSSLCipherSuites: [SSLCipherSuite]?
#endif
//----------------------------------------------------------------------
// MARK: - Private Attributes
//----------------------------------------------------------------------
/// Callbacks for socket state changes
let stateChangeCallbacks: StateChangeCallbacks = StateChangeCallbacks()
/// Collection on channels created for the Socket
public var channels: [Channel] { _channels.copy() }
private var _channels = SynchronizedArray<Channel>()
/// Buffers messages that need to be sent once the socket has connected. It is an array
/// of tuples, with the ref of the message to send and the callback that will send the message.
let sendBuffer = SynchronizedArray<(ref: String?, callback: () throws -> ())>()
/// Ref counter for messages
var ref: UInt64 = UInt64.min // 0 (max: 18,446,744,073,709,551,615)
/// Timer that triggers sending new Heartbeat messages
var heartbeatTimer: HeartbeatTimer?
/// Ref counter for the last heartbeat that was sent
var pendingHeartbeatRef: String?
/// Timer to use when attempting to reconnect
var reconnectTimer: TimeoutTimer
/// Close status
var closeStatus: CloseStatus = .unknown
/// The connection to the server
var connection: PhoenixTransport? = nil
//----------------------------------------------------------------------
// MARK: - Initialization
//----------------------------------------------------------------------
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public convenience init(_ endPoint: String,
params: Payload? = nil,
vsn: String = Defaults.vsn) {
self.init(endPoint: endPoint,
transport: { url in return URLSessionTransport(url: url) },
paramsClosure: { params },
vsn: vsn)
}
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public convenience init(_ endPoint: String,
paramsClosure: PayloadClosure?,
vsn: String = Defaults.vsn) {
self.init(endPoint: endPoint,
transport: { url in return URLSessionTransport(url: url) },
paramsClosure: paramsClosure,
vsn: vsn)
}
public init(endPoint: String,
transport: @escaping ((URL) -> PhoenixTransport),
paramsClosure: PayloadClosure? = nil,
vsn: String = Defaults.vsn) {
self.transport = transport
self.paramsClosure = paramsClosure
self.endPoint = endPoint
self.vsn = vsn
self.endPointUrl = Socket.buildEndpointUrl(endpoint: endPoint,
paramsClosure: paramsClosure,
vsn: vsn)
self.reconnectTimer = TimeoutTimer()
self.reconnectTimer.callback.delegate(to: self) { (self) in
self.logItems("Socket attempting to reconnect")
self.teardown(reason: "reconnection") { self.connect() }
}
self.reconnectTimer.timerCalculation
.delegate(to: self) { (self, tries) -> TimeInterval in
let interval = self.reconnectAfter(tries)
self.logItems("Socket reconnecting in \(interval)s")
return interval
}
}
deinit {
reconnectTimer.reset()
}
//----------------------------------------------------------------------
// MARK: - Public
//----------------------------------------------------------------------
/// - return: The socket protocol, wss or ws
public var websocketProtocol: String {
switch endPointUrl.scheme {
case "https": return "wss"
case "http": return "ws"
default: return endPointUrl.scheme ?? ""
}
}
/// - return: True if the socket is connected
public var isConnected: Bool {
return self.connectionState == .open
}
public var isConnecting: Bool {
return self.connectionState == .connecting
}
/// - return: The state of the connect. [.connecting, .open, .closing, .closed]
public var connectionState: PhoenixTransportReadyState {
return self.connection?.readyState ?? .closed
}
/// Connects the Socket. The params passed to the Socket on initialization
/// will be sent through the connection. If the Socket is already connected,
/// then this call will be ignored.
public func connect() {
// Do not attempt to reconnect if the socket is currently connected or in the process of connecting
guard !isConnected && !isConnecting else { return }
// Reset the close status when attempting to connect
self.closeStatus = .unknown
// We need to build this right before attempting to connect as the
// parameters could be built upon demand and change over time
self.endPointUrl = Socket.buildEndpointUrl(endpoint: self.endPoint,
paramsClosure: self.paramsClosure,
vsn: vsn)
self.connection = self.transport(self.endPointUrl)
self.connection?.delegate = self
// self.connection?.disableSSLCertValidation = disableSSLCertValidation
//
// #if os(Linux)
// #else
// self.connection?.security = security
// self.connection?.enabledSSLCipherSuites = enabledSSLCipherSuites
// #endif
self.connection?.connect(with: self.headers)
}
/// Disconnects the socket
///
/// - parameter code: Optional. Closing status code
/// - parameter callback: Optional. Called when disconnected
public func disconnect(code: CloseCode = CloseCode.normal,
reason: String? = nil,
callback: (() -> Void)? = nil) {
// The socket was closed cleanly by the User
self.closeStatus = CloseStatus(closeCode: code.rawValue)
// Reset any reconnects and teardown the socket connection
self.reconnectTimer.reset()
self.teardown(code: code, reason: reason, callback: callback)
}
internal func teardown(code: CloseCode = CloseCode.normal, reason: String? = nil, callback: (() -> Void)? = nil) {
self.connection?.delegate = nil
self.connection?.disconnect(code: code.rawValue, reason: reason)
self.connection = nil
// The socket connection has been torndown, heartbeats are not needed
self.heartbeatTimer?.stop()
// Since the connection's delegate was nil'd out, inform all state
// callbacks that the connection has closed
self.stateChangeCallbacks.close.forEach({ $0.callback.call((code.rawValue, reason)) })
callback?()
}
//----------------------------------------------------------------------
// MARK: - Register Socket State Callbacks
//----------------------------------------------------------------------
/// Registers callbacks for connection open events. Does not handle retain
/// cycles. Use `delegateOnOpen(to:)` for automatic handling of retain cycles.
///
/// Example:
///
/// socket.onOpen() { [weak self] in
/// self?.print("Socket Connection Open")
/// }
///
/// - parameter callback: Called when the Socket is opened
@discardableResult
public func onOpen(callback: @escaping () -> Void) -> String {
return self.onOpen { _ in callback() }
}
/// Registers callbacks for connection open events. Does not handle retain
/// cycles. Use `delegateOnOpen(to:)` for automatic handling of retain cycles.
///
/// Example:
///
/// socket.onOpen() { [weak self] response in
/// self?.print("Socket Connection Open")
/// }
///
/// - parameter callback: Called when the Socket is opened
@discardableResult
public func onOpen(callback: @escaping (URLResponse?) -> Void) -> String {
var delegated = Delegated<URLResponse?, Void>()
delegated.manuallyDelegate(with: callback)
return self.append(callback: delegated, to: self.stateChangeCallbacks.open)
}
/// Registers callbacks for connection open events. Automatically handles
/// retain cycles. Use `onOpen()` to handle yourself.
///
/// Example:
///
/// socket.delegateOnOpen(to: self) { self in
/// self.print("Socket Connection Open")
/// }
///
/// - parameter owner: Class registering the callback. Usually `self`
/// - parameter callback: Called when the Socket is opened
@discardableResult
public func delegateOnOpen<T: AnyObject>(to owner: T,
callback: @escaping ((T) -> Void)) -> String {
return self.delegateOnOpen(to: owner) { owner, _ in callback(owner) }
}
/// Registers callbacks for connection open events. Automatically handles
/// retain cycles. Use `onOpen()` to handle yourself.
///
/// Example:
///
/// socket.delegateOnOpen(to: self) { self, response in
/// self.print("Socket Connection Open")
/// }
///
/// - parameter owner: Class registering the callback. Usually `self`
/// - parameter callback: Called when the Socket is opened
@discardableResult
public func delegateOnOpen<T: AnyObject>(to owner: T,
callback: @escaping ((T, URLResponse?) -> Void)) -> String {
var delegated = Delegated<URLResponse?, Void>()
delegated.delegate(to: owner, with: callback)
return self.append(callback: delegated, to: self.stateChangeCallbacks.open)
}
/// Registers callbacks for connection close events. Does not handle retain
/// cycles. Use `delegateOnClose(_:)` for automatic handling of retain cycles.
///
/// Example:
///
/// socket.onClose() { [weak self] in
/// self?.print("Socket Connection Close")
/// }
///
/// - parameter callback: Called when the Socket is closed
@discardableResult
public func onClose(callback: @escaping () -> Void) -> String {
return self.onClose { _, _ in callback() }
}
/// Registers callbacks for connection close events. Does not handle retain
/// cycles. Use `delegateOnClose(_:)` for automatic handling of retain cycles.
///
/// Example:
///
/// socket.onClose() { [weak self] code, reason in
/// self?.print("Socket Connection Close")
/// }
///
/// - parameter callback: Called when the Socket is closed
@discardableResult
public func onClose(callback: @escaping (Int, String?) -> Void) -> String {
var delegated = Delegated<(Int, String?), Void>()
delegated.manuallyDelegate(with: callback)
return self.append(callback: delegated, to: self.stateChangeCallbacks.close)
}
/// Registers callbacks for connection close events. Automatically handles
/// retain cycles. Use `onClose()` to handle yourself.
///
/// Example:
///
/// socket.delegateOnClose(self) { self in
/// self.print("Socket Connection Close")
/// }
///
/// - parameter owner: Class registering the callback. Usually `self`
/// - parameter callback: Called when the Socket is closed
@discardableResult
public func delegateOnClose<T: AnyObject>(to owner: T,
callback: @escaping ((T) -> Void)) -> String {
return self.delegateOnClose(to: owner) { owner, _ in callback(owner) }
}
/// Registers callbacks for connection close events. Automatically handles
/// retain cycles. Use `onClose()` to handle yourself.
///
/// Example:
///
/// socket.delegateOnClose(self) { self, code, reason in
/// self.print("Socket Connection Close")
/// }
///
/// - parameter owner: Class registering the callback. Usually `self`
/// - parameter callback: Called when the Socket is closed
@discardableResult
public func delegateOnClose<T: AnyObject>(to owner: T,
callback: @escaping ((T, (Int, String?)) -> Void)) -> String {
var delegated = Delegated<(Int, String?), Void>()
delegated.delegate(to: owner, with: callback)
return self.append(callback: delegated, to: self.stateChangeCallbacks.close)
}
/// Registers callbacks for connection error events. Does not handle retain
/// cycles. Use `delegateOnError(to:)` for automatic handling of retain cycles.
///
/// Example:
///
/// socket.onError() { [weak self] (error) in
/// self?.print("Socket Connection Error", error)
/// }
///
/// - parameter callback: Called when the Socket errors
@discardableResult
public func onError(callback: @escaping ((Error, URLResponse?)) -> Void) -> String {
var delegated = Delegated<(Error, URLResponse?), Void>()
delegated.manuallyDelegate(with: callback)
return self.append(callback: delegated, to: self.stateChangeCallbacks.error)
}
/// Registers callbacks for connection error events. Automatically handles
/// retain cycles. Use `manualOnError()` to handle yourself.
///
/// Example:
///
/// socket.delegateOnError(to: self) { (self, error) in
/// self.print("Socket Connection Error", error)
/// }
///
/// - parameter owner: Class registering the callback. Usually `self`
/// - parameter callback: Called when the Socket errors
@discardableResult
public func delegateOnError<T: AnyObject>(to owner: T,
callback: @escaping ((T, (Error, URLResponse?)) -> Void)) -> String {
var delegated = Delegated<(Error, URLResponse?), Void>()
delegated.delegate(to: owner, with: callback)
return self.append(callback: delegated, to: self.stateChangeCallbacks.error)
}
/// Registers callbacks for connection message events. Does not handle
/// retain cycles. Use `delegateOnMessage(_to:)` for automatic handling of
/// retain cycles.
///
/// Example:
///
/// socket.onMessage() { [weak self] (message) in
/// self?.print("Socket Connection Message", message)
/// }
///
/// - parameter callback: Called when the Socket receives a message event
@discardableResult
public func onMessage(callback: @escaping (Message) -> Void) -> String {
var delegated = Delegated<Message, Void>()
delegated.manuallyDelegate(with: callback)
return self.append(callback: delegated, to: self.stateChangeCallbacks.message)
}
/// Registers callbacks for connection message events. Automatically handles
/// retain cycles. Use `onMessage()` to handle yourself.
///
/// Example:
///
/// socket.delegateOnMessage(self) { (self, message) in
/// self.print("Socket Connection Message", message)
/// }
///
/// - parameter owner: Class registering the callback. Usually `self`
/// - parameter callback: Called when the Socket receives a message event
@discardableResult
public func delegateOnMessage<T: AnyObject>(to owner: T,
callback: @escaping ((T, Message) -> Void)) -> String {
var delegated = Delegated<Message, Void>()
delegated.delegate(to: owner, with: callback)
return self.append(callback: delegated, to: self.stateChangeCallbacks.message)
}
private func append<T>(callback: T, to array: SynchronizedArray<(ref: String, callback: T)>) -> String {
let ref = makeRef()
array.append((ref, callback))
return ref
}
/// Releases all stored callback hooks (onError, onOpen, onClose, etc.) You should
/// call this method when you are finished when the Socket in order to release
/// any references held by the socket.
public func releaseCallbacks() {
self.stateChangeCallbacks.open.removeAll()
self.stateChangeCallbacks.close.removeAll()
self.stateChangeCallbacks.error.removeAll()
self.stateChangeCallbacks.message.removeAll()
}
//----------------------------------------------------------------------
// MARK: - Channel Initialization
//----------------------------------------------------------------------
/// Initialize a new Channel
///
/// Example:
///
/// let channel = socket.channel("rooms", params: ["user_id": "abc123"])
///
/// - parameter topic: Topic of the channel
/// - parameter params: Optional. Parameters for the channel
/// - return: A new channel
public func channel(_ topic: String,
params: [String: Any] = [:]) -> Channel {
let channel = Channel(topic: topic, params: params, socket: self)
_channels.append(channel)
return channel
}
/// Removes the Channel from the socket. This does not cause the channel to
/// inform the server that it is leaving. You should call channel.leave()
/// prior to removing the Channel.
///
/// Example:
///
/// channel.leave()
/// socket.remove(channel)
///
/// - parameter channel: Channel to remove
public func remove(_ channel: Channel) {
self.off(channel.stateChangeRefs)
_channels.removeAll(where: { $0.joinRef == channel.joinRef })
}
/// Removes `onOpen`, `onClose`, `onError,` and `onMessage` registrations.
///
///
/// - Parameter refs: List of refs returned by calls to `onOpen`, `onClose`, etc
public func off(_ refs: [String]) {
self.stateChangeCallbacks.open.removeAll { refs.contains($0.ref) }
self.stateChangeCallbacks.close.removeAll { refs.contains($0.ref) }
self.stateChangeCallbacks.error.removeAll { refs.contains($0.ref) }
self.stateChangeCallbacks.message.removeAll { refs.contains($0.ref) }
}
//----------------------------------------------------------------------
// MARK: - Sending Data
//----------------------------------------------------------------------
/// Sends data through the Socket. This method is internal. Instead, you
/// should call `push(_:, payload:, timeout:)` on the Channel you are
/// sending an event to.
///
/// - parameter topic:
/// - parameter event:
/// - parameter payload:
/// - parameter ref: Optional. Defaults to nil
/// - parameter joinRef: Optional. Defaults to nil
internal func push(topic: String,
event: String,
payload: Payload,
ref: String? = nil,
joinRef: String? = nil) {
let callback: (() throws -> ()) = { [weak self] in
guard let self else { return }
let body: [Any?] = [joinRef, ref, topic, event, payload]
let data = self.encode(body)
self.logItems("push", "Sending \(String(data: data, encoding: String.Encoding.utf8) ?? "")" )
self.connection?.send(data: data)
}
/// If the socket is connected, then execute the callback immediately.
if isConnected {
try? callback()
} else {
/// If the socket is not connected, add the push to a buffer which will
/// be sent immediately upon connection.
self.sendBuffer.append((ref: ref, callback: callback))
}
}
/// - return: the next message ref, accounting for overflows
public func makeRef() -> String {
self.ref = (ref == UInt64.max) ? 0 : self.ref + 1
return String(ref)
}
/// Logs the message. Override Socket.logger for specialized logging. noops by default
///
/// - parameter items: List of items to be logged. Behaves just like debugPrint()
func logItems(_ items: Any...) {
let msg = items.map( { return String(describing: $0) } ).joined(separator: ", ")
self.logger?("SwiftPhoenixClient: \(msg)")
}
//----------------------------------------------------------------------
// MARK: - Connection Events
//----------------------------------------------------------------------
/// Called when the underlying Websocket connects to it's host
internal func onConnectionOpen(response: URLResponse?) {
self.logItems("transport", "Connected to \(endPoint)")
// Reset the close status now that the socket has been connected
self.closeStatus = .unknown
// Send any messages that were waiting for a connection
self.flushSendBuffer()
// Reset how the socket tried to reconnect
self.reconnectTimer.reset()
// Restart the heartbeat timer
self.resetHeartbeat()
// Inform all onOpen callbacks that the Socket has opened
self.stateChangeCallbacks.open.forEach({ $0.callback.call((response)) })
}
internal func onConnectionClosed(code: Int, reason: String?) {
self.logItems("transport", "close")
// Send an error to all channels
self.triggerChannelError()
// Prevent the heartbeat from triggering if the
self.heartbeatTimer?.stop()
// Only attempt to reconnect if the socket did not close normally,
// or if it was closed abnormally but on client side (e.g. due to heartbeat timeout)
if (self.closeStatus.shouldReconnect) {
self.reconnectTimer.scheduleTimeout()
}
self.stateChangeCallbacks.close.forEach({ $0.callback.call((code, reason)) })
}
internal func onConnectionError(_ error: Error, response: URLResponse?) {
self.logItems("transport", error, response ?? "")
// Send an error to all channels
self.triggerChannelError()
// Inform any state callbacks of the error
self.stateChangeCallbacks.error.forEach({ $0.callback.call((error, response)) })
}
internal func onConnectionMessage(_ rawMessage: String) {
self.logItems("receive ", rawMessage)
guard
let data = rawMessage.data(using: String.Encoding.utf8),
let json = decode(data) as? [Any?],
let message = Message(json: json)
else {
self.logItems("receive: Unable to parse JSON: \(rawMessage)")
return }
// Clear heartbeat ref, preventing a heartbeat timeout disconnect
if message.ref == pendingHeartbeatRef { pendingHeartbeatRef = nil }
if message.event == "phx_close" {
print("Close Event Received")
}
// Dispatch the message to all channels that belong to the topic
_channels.forEach { channel in
if channel.isMember(message) {
channel.trigger(message)
}
}
// Inform all onMessage callbacks of the message
self.stateChangeCallbacks.message.forEach({ $0.callback.call(message) })
}
/// Triggers an error event to all of the connected Channels
internal func triggerChannelError() {
_channels.forEach { channel in
// Only trigger a channel error if it is in an "opened" state
if !(channel.isErrored || channel.isLeaving || channel.isClosed) {
channel.trigger(event: ChannelEvent.error)
}
}
}
/// Send all messages that were buffered before the socket opened
internal func flushSendBuffer() {
guard isConnected else { return }
self.sendBuffer.forEach( { try? $0.callback() } )
self.sendBuffer.removeAll()
}
/// Removes an item from the sendBuffer with the matching ref
internal func removeFromSendBuffer(ref: String) {
self.sendBuffer.removeAll { $0.ref == ref }
}
/// Builds a fully qualified socket `URL` from `endPoint` and `params`.
internal static func buildEndpointUrl(endpoint: String, paramsClosure params: PayloadClosure?, vsn: String) -> URL {
guard
let url = URL(string: endpoint),
var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
else { fatalError("Malformed URL: \(endpoint)") }
// Ensure that the URL ends with "/websocket
if !urlComponents.path.contains("/websocket") {
// Do not duplicate '/' in the path
if urlComponents.path.last != "/" {
urlComponents.path.append("/")
}
// append 'websocket' to the path
urlComponents.path.append("websocket")
}
urlComponents.queryItems = [URLQueryItem(name: "vsn", value: vsn)]
// If there are parameters, append them to the URL
if let params = params?() {
urlComponents.queryItems?.append(contentsOf: params.map {
URLQueryItem(name: $0.key, value: String(describing: $0.value))
})
}
guard let qualifiedUrl = urlComponents.url
else { fatalError("Malformed URL while adding parameters") }
return qualifiedUrl
}
// Leaves any channel that is open that has a duplicate topic
internal func leaveOpenTopic(topic: String) {
guard
let dupe = _channels.first(where: { $0.topic == topic && ($0.isJoined || $0.isJoining) })
else { return }
self.logItems("transport", "leaving duplicate topic: [\(topic)]" )
dupe.leave()
}
//----------------------------------------------------------------------
// MARK: - Heartbeat
//----------------------------------------------------------------------
internal func resetHeartbeat() {
// Clear anything related to the heartbeat
self.pendingHeartbeatRef = nil
self.heartbeatTimer?.stop()
// Do not start up the heartbeat timer if skipHeartbeat is true
guard !skipHeartbeat else { return }
self.heartbeatTimer = HeartbeatTimer(timeInterval: heartbeatInterval, leeway: heartbeatLeeway)
self.heartbeatTimer?.start(eventHandler: { [weak self] in
self?.sendHeartbeat()
})
}
/// Sends a heartbeat payload to the phoenix servers
@objc func sendHeartbeat() {
// Do not send if the connection is closed
guard isConnected else { return }
// If there is a pending heartbeat ref, then the last heartbeat was
// never acknowledged by the server. Close the connection and attempt
// to reconnect.
if let _ = self.pendingHeartbeatRef {
self.pendingHeartbeatRef = nil
self.logItems("transport",
"heartbeat timeout. Attempting to re-establish connection")
// Close the socket manually, flagging the closure as abnormal. Do not use
// `teardown` or `disconnect` as they will nil out the websocket delegate.
self.abnormalClose("heartbeat timeout")
return
}
// The last heartbeat was acknowledged by the server. Send another one
self.pendingHeartbeatRef = self.makeRef()
self.push(topic: "phoenix",
event: ChannelEvent.heartbeat,
payload: [:],
ref: self.pendingHeartbeatRef)
}
internal func abnormalClose(_ reason: String) {
self.closeStatus = .abnormal
/*
We use NORMAL here since the client is the one determining to close the
connection. However, we set to close status to abnormal so that
the client knows that it should attempt to reconnect.
If the server subsequently acknowledges with code 1000 (normal close),
the socket will keep the `.abnormal` close status and trigger a reconnection.
*/
self.connection?.disconnect(code: CloseCode.normal.rawValue, reason: reason)
}
//----------------------------------------------------------------------
// MARK: - TransportDelegate
//----------------------------------------------------------------------
public func onOpen(response: URLResponse?) {
self.onConnectionOpen(response: response)
}
public func onError(error: Error, response: URLResponse?) {
self.onConnectionError(error, response: response)
}
public func onMessage(message: String) {
self.onConnectionMessage(message)
}
public func onClose(code: Int, reason: String? = nil) {
self.closeStatus.update(transportCloseCode: code)
self.onConnectionClosed(code: code, reason: reason)
}
}
//----------------------------------------------------------------------
// MARK: - Close Codes
//----------------------------------------------------------------------
extension Socket {
public enum CloseCode : Int {
case abnormal = 999
case normal = 1000
case goingAway = 1001
}
}
//----------------------------------------------------------------------
// MARK: - Close Status
//----------------------------------------------------------------------
extension Socket {
/// Indicates the different closure states a socket can be in.
enum CloseStatus {
/// Undetermined closure state
case unknown
/// A clean closure requested either by the client or the server
case clean
/// An abnormal closure requested by the client
case abnormal
/// Temporarily close the socket, pausing reconnect attempts. Useful on mobile
/// clients when disconnecting a because the app resigned active but should
/// reconnect when app enters active state.
case temporary
init(closeCode: Int) {
switch closeCode {
case CloseCode.abnormal.rawValue:
self = .abnormal
case CloseCode.goingAway.rawValue:
self = .temporary
default:
self = .clean
}
}
mutating func update(transportCloseCode: Int) {
switch self {
case .unknown, .clean, .temporary:
// Allow transport layer to override these statuses.
self = .init(closeCode: transportCloseCode)
case .abnormal:
// Do not allow transport layer to override the abnormal close status.
// The socket itself should reset it on the next connection attempt.
// See `Socket.abnormalClose(_:)` for more information.
break
}
}
var shouldReconnect: Bool {
switch self {
case .unknown, .abnormal:
return true
case .clean, .temporary:
return false
}
}
}
}
================================================
FILE: Sources/SwiftPhoenixClient/SynchronizedArray.swift
================================================
//
// SynchronizedArray.swift
// SwiftPhoenixClient
//
// Created by Daniel Rees on 4/12/23.
// Copyright © 2023 SwiftPhoenixClient. All rights reserved.
//
import Foundation
/// A thread-safe array.
public class SynchronizedArray<Element> {
fileprivate let queue = DispatchQueue(label: "spc_sync_array", attributes: .concurrent)
fileprivate var array: [Element]
public init(_ array: [Element] = []) {
self.array = array
}
public func copy() -> [Element] {
queue.sync { self.array }
}
func append( _ newElement: Element) {
queue.async(flags: .barrier) {
self.array.append(newElement)
}
}
func first(where predicate: (Element) -> Bool) -> Element? {
queue.sync { self.array.first(where: predicate) }
}
func forEach(_ body: (Element) -> Void) {
queue.sync { self.array }.forEach(body)
}
func removeAll() {
queue.async(flags: .barrier) {
self.array.removeAll()
}
}
func removeAll(where shouldBeRemoved: @escaping (Element) -> Bool) {
queue.async(flags: .barrier) {
self.array.removeAll(where: shouldBeRemoved)
}
}
}
================================================
FILE: Sources/SwiftPhoenixClient/TimeoutTimer.swift
================================================
// Copyright (c) 2021 David Stump <david@davidstump.net>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
/// Creates a timer that can perform calculated reties by setting
/// `timerCalculation` , such as exponential backoff.
///
/// ### Example
///
/// let reconnectTimer = TimeoutTimer()
///
/// // Receive a callbcak when the timer is fired
/// reconnectTimer.callback.delegate(to: self) { (_) in
/// print("timer was fired")
/// }
///
/// // Provide timer interval calculation
/// reconnectTimer.timerCalculation.delegate(to: self) { (_, tries) -> TimeInterval in
/// return tries > 2 ? 1000 : [1000, 5000, 10000][tries - 1]
/// }
///
/// reconnectTimer.scheduleTimeout() // fires after 1000ms
/// reconnectTimer.scheduleTimeout() // fires after 5000ms
/// reconnectTimer.reset()
/// reconnectTimer.scheduleTimeout() // fires after 1000ms
import Foundation
// sourcery: AutoMockable
class TimeoutTimer {
/// Callback to be informed when the underlying Timer fires
var callback = Delegated<(), Void>()
/// Provides TimeInterval to use when scheduling the timer
var timerCalculation = Delegated<Int, TimeInterval>()
/// The work to be done when the queue fires
var workItem: DispatchWorkItem? = nil
/// The number of times the underlyingTimer hass been set off.
var tries: Int = 0
/// The Queue to execute on. In testing, this is overridden
var queue: TimerQueue = TimerQueue.main
/// Resets the Timer, clearing the number of tries and stops
/// any scheduled timeout.
func reset() {
self.tries = 0
self.clearTimer()
}
/// Schedules a timeout callback to fire after a calculated timeout duration.
func scheduleTimeout() {
// Clear any ongoing timer, not resetting the number of tries
self.clearTimer()
// Get the next calculated interval, in milliseconds. Do not
// start the timer if the interval is returned as nil.
guard let timeInterval
= self.timerCalculation.call(self.tries + 1) else { return }
let workItem = DispatchWorkItem {
self.tries += 1
self.callback.call()
}
self.workItem = workItem
self.queue.queue(timeInterval: timeInterval, execute: workItem)
}
/// Invalidates any ongoing Timer. Will not clear how many tries have been made
private func clearTimer() {
self.workItem?.cancel()
self.workItem = nil
}
}
/// Wrapper class around a DispatchQueue. Allows for providing a fake clock
/// during tests.
class TimerQueue {
// Can be overriden in tests
static var main = TimerQueue()
func queue(timeInterval: TimeInterval, execute: DispatchWorkItem) {
// TimeInterval is always in seconds. Multiply it by 1000 to convert
// to milliseconds and round to the nearest millisecond.
let dispatchInterval = Int(round(timeInterval * 1000))
let dispatchTime = DispatchTime.now() + .milliseconds(dispatchInterval)
DispatchQueue.main.asyncAfter(deadline: dispatchTime, execute: execute)
}
}
================================================
FILE: SwiftPhoenixClient.podspec
================================================
#
# Be sure to run `pod lib lint SwiftPhoenixClient.podspec' to ensure this is a
# valid spec before submitting.
#
# Any lines starting with a # are optional, but their use is encouraged
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = "SwiftPhoenixClient"
s.version = "5.3.5"
s.summary = "Connect your Phoenix and iOS applications through WebSockets!"
s.swift_version = "5.0"
s.description = <<-EOS
SwiftPhoenixClient is a Swift port of phoenix.js, abstracting away the details
of the Phoenix Channels library and providing a near identical experience
to connect to your Phoenix WebSockets on iOS.
RxSwift extensions exist as well when subscribing to channel events.
A default Transport layer is implmenented for iOS 13 or later. If targeting
an earlier iOS version, please see the StarscreamSwiftPhoenixClient extention.
EOS
s.homepage = "https://github.com/davidstump/SwiftPhoenixClient"
s.license = { :type => "MIT", :file => "LICENSE" }
s.author = { "David Stump" => "david@davidstump.net" }
s.source = { :git => "https://github.com/davidstump/SwiftPhoenixClient.git", :tag => s.version.to_s }
s.ios.deployment_target = '11.0'
s.osx.deployment_target = '10.13'
s.tvos.deployment_target = '11.0'
s.watchos.deployment_target = '4.0'
s.swift_version = '5.0'
s.source_files = "Sources/SwiftPhoenixClient/"
end
================================================
FILE: SwiftPhoenixClient.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
125A9EF82543C84700292017 /* SwiftPhoenixClient.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1281439225410D52008615A7 /* SwiftPhoenixClient.framework */; };
125A9F082543C8D000292017 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 125A9EFF2543C8D000292017 /* Assets.xcassets */; };
125A9F092543C8D000292017 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 125A9F002543C8D000292017 /* LaunchScreen.storyboard */; };
125A9F0A2543C8D000292017 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 125A9F022543C8D000292017 /* Main.storyboard */; };
125A9F0B2543C8D000292017 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 125A9F042543C8D000292017 /* AppDelegate.swift */; };
125A9F0D2543C8D000292017 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 125A9F062543C8D000292017 /* SceneDelegate.swift */; };
125A9F0F2543CDDE00292017 /* BasicChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 125A9F0E2543CDDE00292017 /* BasicChatViewController.swift */; };
1281439C25410D52008615A7 /* SwiftPhoenixClient.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1281439225410D52008615A7 /* SwiftPhoenixClient.framework */; };
128143B225410DF3008615A7 /* SwiftPhoenixClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 128143B025410DF3008615A7 /* SwiftPhoenixClient.h */; settings = {ATTRIBUTES = (Public, ); }; };
12922E202543B10B0034B257 /* Socket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12922E1F2543B10B0034B257 /* Socket.swift */; };
12922E222543B37B0034B257 /* PhoenixTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12922E212543B37B0034B257 /* PhoenixTransport.swift */; };
12991DFB254C3EB800BB8650 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991DFA254C3EB800BB8650 /* Defaults.swift */; };
12991DFD254C3ED300BB8650 /* Delegated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991DFC254C3ED300BB8650 /* Delegated.swift */; };
12991DFF254C3EF900BB8650 /* HeartbeatTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991DFE254C3EF900BB8650 /* HeartbeatTimer.swift */; };
12991E01254C3F1300BB8650 /* TimeoutTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991E00254C3F1300BB8650 /* TimeoutTimer.swift */; };
12991E03254C3F2300BB8650 /* Push.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991E02254C3F2300BB8650 /* Push.swift */; };
12991E05254C3F3300BB8650 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991E04254C3F3300BB8650 /* Channel.swift */; };
12991E07254C3F4B00BB8650 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991E06254C3F4B00BB8650 /* Message.swift */; };
12991E09254C3F5B00BB8650 /* Presence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991E08254C3F5B00BB8650 /* Presence.swift */; };
12991E0F254C454C00BB8650 /* SocketSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991E0E254C454C00BB8650 /* SocketSpy.swift */; };
12991E11254C456F00BB8650 /* FakeTimerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991E10254C456F00BB8650 /* FakeTimerQueue.swift */; };
12991E13254C458100BB8650 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991E12254C458100BB8650 /* TestHelpers.swift */; };
12991E15254C45FC00BB8650 /* FakeTimerQueueSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991E14254C45FC00BB8650 /* FakeTimerQueueSpec.swift */; };
12EF620425524B6800A6EE9B /* SocketSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EF61FF25524B6800A6EE9B /* SocketSpec.swift */; };
12EF620525524B6800A6EE9B /* ChannelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EF620025524B6800A6EE9B /* ChannelSpec.swift */; };
12EF620625524B6800A6EE9B /* PresenceSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EF620125524B6800A6EE9B /* PresenceSpec.swift */; };
12EF620725524B6800A6EE9B /* TimeoutTimerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EF620225524B6800A6EE9B /* TimeoutTimerSpec.swift */; };
12EF620825524B6800A6EE9B /* DefaultSerializerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EF620325524B6800A6EE9B /* DefaultSerializerSpec.swift */; };
12EF620E25524EEF00A6EE9B /* MockableClass.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EF620C25524EEF00A6EE9B /* MockableClass.generated.swift */; };
12EF620F25524EEF00A6EE9B /* MockableProtocol.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EF620D25524EEF00A6EE9B /* MockableProtocol.generated.swift */; };
635669C4261631DC0068B665 /* URLSessionTransportSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 635669C3261631DC0068B665 /* URLSessionTransportSpec.swift */; };
63ACBE9426D53DF500171582 /* HeartbeatTimerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63ACBE9326D53DF500171582 /* HeartbeatTimerSpec.swift */; };
63B526DE2656D53700289719 /* Nimble.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63B526DC2656D53700289719 /* Nimble.xcframework */; };
63B526DF2656D53700289719 /* Quick.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63B526DD2656D53700289719 /* Quick.xcframework */; };
63DE6CCA272A2ECB00E2A728 /* MessageSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DE6CC9272A2ECB00E2A728 /* MessageSpec.swift */; };
63F0F58D2592E44800C904FB /* ChatRoomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F0F58C2592E44800C904FB /* ChatRoomViewController.swift */; };
63F3765329E7296F00A5AB6E /* SynchronizedArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F3765229E7296F00A5AB6E /* SynchronizedArray.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
125A9EFA2543C84700292017 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 1281438925410D52008615A7 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 1281439125410D52008615A7;
remoteInfo = SwiftPhoenixClient;
};
1281439D25410D52008615A7 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 1281438925410D52008615A7 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 1281439125410D52008615A7;
remoteInfo = SwiftPhoenixClient;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
63B526B42656D0DE00289719 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
125A9EE42543C82800292017 /* Basic.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Basic.app; sourceTree = BUILT_PRODUCTS_DIR; };
125A9EFF2543C8D000292017 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
125A9F012543C8D000292017 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
125A9F032543C8D000292017 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
125A9F042543C8D000292017 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
125A9F052543C8D000292017 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
125A9F062543C8D000292017 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
125A9F0E2543CDDE00292017 /* BasicChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicChatViewController.swift; sourceTree = "<group>"; };
1281439225410D52008615A7 /* SwiftPhoenixClient.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftPhoenixClient.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1281439B25410D52008615A7 /* SwiftPhoenixClientTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftPhoenixClientTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
128143B025410DF3008615A7 /* SwiftPhoenixClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SwiftPhoenixClient.h; sourceTree = "<group>"; };
128143B125410DF3008615A7 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
12922E1F2543B10B0034B257 /* Socket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Socket.swift; sourceTree = "<group>"; };
12922E212543B37B0034B257 /* PhoenixTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoenixTransport.swift; sourceTree = "<group>"; };
12991DFA254C3EB800BB8650 /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = "<group>"; };
12991DFC254C3ED300BB8650 /* Delegated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delegated.swift; sourceTree = "<group>"; };
12991DFE254C3EF900BB8650 /* HeartbeatTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeartbeatTimer.swift; sourceTree = "<group>"; };
12991E00254C3F1300BB8650 /* TimeoutTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeoutTimer.swift; sourceTree = "<group>"; };
12991E02254C3F2300BB8650 /* Push.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Push.swift; sourceTree = "<group>"; };
12991E04254C3F3300BB8650 /* Channel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channel.swift; sourceTree = "<group>"; };
12991E06254C3F4B00BB8650 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; };
12991E08254C3F5B00BB8650 /* Presence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presence.swift; sourceTree = "<group>"; };
12991E0E254C454C00BB8650 /* SocketSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketSpy.swift; sourceTree = "<group>"; };
12991E10254C456F00BB8650 /* FakeTimerQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeTimerQueue.swift; sourceTree = "<group>"; };
12991E12254C458100BB8650 /* TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = "<group>"; };
12991E14254C45FC00BB8650 /* FakeTimerQueueSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeTimerQueueSpec.swift; sourceTree = "<group>"; };
12EF61FF25524B6800A6EE9B /* SocketSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketSpec.swift; sourceTree = "<group>"; };
12EF620025524B6800A6EE9B /* ChannelSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelSpec.swift; sourceTree = "<group>"; };
12EF620125524B6800A6EE9B /* PresenceSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenceSpec.swift; sourceTree = "<group>"; };
12EF620225524B6800A6EE9B /* TimeoutTimerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeoutTimerSpec.swift; sourceTree = "<group>"; };
12EF620325524B6800A6EE9B /* DefaultSerializerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultSerializerSpec.swift; sourceTree = "<group>"; };
12EF620C25524EEF00A6EE9B /* MockableClass.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockableClass.generated.swift; sourceTree = "<group>"; };
12EF620D25524EEF00A6EE9B /* MockableProtocol.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockableProtocol.generated.swift; sourceTree = "<group>"; };
635669C3261631DC0068B665 /* URLSessionTransportSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTransportSpec.swift; sourceTree = "<group>"; };
63ACBE9326D53DF500171582 /* HeartbeatTimerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeartbeatTimerSpec.swift; sourceTree = "<group>"; };
63B526842656CF9600289719 /* Starscream.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Starscream.xcframework; path = Carthage/Build/Starscream.xcframework; sourceTree = "<group>"; };
63B5268D2656CFFE00289719 /* RxSwift.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = RxSwift.xcframework; path = Carthage/Build/RxSwift.xcframework; sourceTree = "<group>"; };
63B526DC2656D53700289719 /* Nimble.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Nimble.xcframework; path = Carthage/Build/Nimble.xcframework; sourceTree = "<group>"; };
63B526DD2656D53700289719 /* Quick.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Quick.xcframework; path = Carthage/Build/Quick.xcframework; sourceTree = "<group>"; };
63DE6CC9272A2ECB00E2A728 /* MessageSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSpec.swift; sourceTree = "<group>"; };
63F0F58C2592E44800C904FB /* ChatRoomViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRoomViewController.swift; sourceTree = "<group>"; };
63F3765229E7296F00A5AB6E /* SynchronizedArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedArray.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
125A9EE12543C82800292017 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
125A9EF82543C84700292017 /* SwiftPhoenixClient.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
1281438F25410D52008615A7 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
1281439825410D52008615A7 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
63B526DE2656D53700289719 /* Nimble.xcframework in Frameworks */,
63B526DF2656D53700289719 /* Quick.xcframework in Frameworks */,
1281439C25410D52008615A7 /* SwiftPhoenixClient.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
125A9EFD2543C8D000292017 /* Basic */ = {
isa = PBXGroup;
children = (
63F0F5922592E59800C904FB /* chatroom */,
63F0F5912592E58800C904FB /* basic */,
125A9EFF2543C8D000292017 /* Assets.xcassets */,
125A9F002543C8D000292017 /* LaunchScreen.storyboard */,
125A9F022543C8D000292017 /* Main.storyboard */,
125A9F042543C8D000292017 /* AppDelegate.swift */,
125A9F052543C8D000292017 /* Info.plist */,
125A9F062543C8D000292017 /* SceneDelegate.swift */,
);
path = Basic;
sourceTree = "<group>";
};
1281438825410D52008615A7 = {
isa = PBXGroup;
children = (
128143AC25410DF3008615A7 /* Sources */,
128143B425410E01008615A7 /* Tests */,
128143BB25410E13008615A7 /* Examples */,
128143D82541142F008615A7 /* Frameworks */,
1281439325410D52008615A7 /* Products */,
);
sourceTree = "<group>";
};
1281439325410D52008615A7 /* Products */ = {
isa = PBXGroup;
children = (
1281439225410D52008615A7 /* SwiftPhoenixClient.framework */,
1281439B25410D52008615A7 /* SwiftPhoenixClientTests.xctest */,
125A9EE42543C82800292017 /* Basic.app */,
);
name = Products;
sourceTree = "<group>";
};
128143AC25410DF3008615A7 /* Sources */ = {
isa = PBXGroup;
children = (
128143AE25410DF3008615A7 /* SwiftPhoenixClient */,
128143AF25410DF3008615A7 /* Supporting Files */,
);
path = Sources;
sourceTree = "<group>";
};
128143AE25410DF3008615A7 /* SwiftPhoenixClient */ = {
isa = PBXGroup;
children = (
12991E04254C3F3300BB8650 /* Channel.swift */,
12991DFA254C3EB800BB8650 /* Defaults.swift */,
12991DFC254C3ED300BB8650 /* Delegated.swift */,
12991DFE254C3EF900BB8650 /* HeartbeatTimer.swift */,
12991E06254C3F4B00BB8650 /* Message.swift */,
12991E08254C3F5B00BB8650 /* Presence.swift */,
12991E02254C3F2300BB8650 /* Push.swift */,
12922E1F2543B10B0034B257 /* Socket.swift */,
12991E00254C3F1300BB8650 /* TimeoutTimer.swift */,
12922E212543B37B0034B257 /* PhoenixTransport.swift */,
63F3765229E7296F00A5AB6E /* SynchronizedArray.swift */,
);
path = SwiftPhoenixClient;
sourceTree = "<group>";
};
128143AF25410DF3008615A7 /* Supporting Files */ = {
isa = PBXGroup;
children = (
128143B025410DF3008615A7 /* SwiftPhoenixClient.h */,
128143B125410DF3008615A7 /* Info.plist */,
);
path = "Supporting Files";
sourceTree = "<group>";
};
128143B425410E01008615A7 /* Tests */ = {
isa = PBXGroup;
children = (
12991E0C254C450A00BB8650 /* Fakes */,
12991E0D254C451700BB8650 /* Helpers */,
12EF620925524DBF00A6EE9B /* Mocks */,
12EF61FE25524B6800A6EE9B /* SwiftPhoenixClientTests */,
);
path = Tests;
sourceTree = "<group>";
};
128143BB25410E13008615A7 /* Examples */ = {
isa = PBXGroup;
children = (
125A9EFD2543C8D000292017 /* Basic */,
);
path = Examples;
sourceTree = "<group>";
};
128143D82541142F008615A7 /* Frameworks */ = {
isa = PBXGroup;
children = (
63B526DC2656D53700289719 /* Nimble.xcframework */,
63B526DD2656D53700289719 /* Quick.xcframework */,
63B5268D2656CFFE00289719 /* RxSwift.xcframework */,
63B526842656CF9600289719 /* Starscream.xcframework */,
);
name = Frameworks;
sourceTree = "<group>";
};
12991E0C254C450A00BB8650 /* Fakes */ = {
isa = PBXGroup;
children = (
12991E0E254C454C00BB8650 /* SocketSpy.swift */,
12991E10254C456F00BB8650 /* FakeTimerQueue.swift */,
12991E14254C45FC00BB8650 /* FakeTimerQueueSpec.swift */,
);
path = Fakes;
sourceTree = "<group>";
};
12991E0D254C451700BB8650 /* Helpers */ = {
isa = PBXGroup;
children = (
12991E12254C458100BB8650 /* TestHelpers.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
12EF61FE25524B6800A6EE9B /* SwiftPhoenixClientTests */ = {
isa = PBXGroup;
children = (
12EF61FF25524B6800A6EE9B /* SocketSpec.swift */,
12EF620025524B6800A6EE9B /* ChannelSpec.swift */,
12EF620125524B6800A6EE9B /* PresenceSpec.swift */,
12EF620225524B6800A6EE9B /* TimeoutTimerSpec.swift */,
12EF620325524B6800A6EE9B /* DefaultSerializerSpec.swift */,
635669C3261631DC0068B665 /* URLSessionTransportSpec.swift */,
63ACBE9326D53DF500171582 /* HeartbeatTimerSpec.swift */,
63DE6CC9272A2ECB00E2A728 /* MessageSpec.swift */,
);
path = SwiftPhoenixClientTests;
sourceTree = "<group>";
};
12EF620925524DBF00A6EE9B /* Mocks */ = {
isa = PBXGroup;
children = (
12EF620C25524EEF00A6EE9B /* MockableClass.g
gitextract_igy9h_6n/
├── .gitignore
├── .slather.yml
├── .sourcery.yml
├── .travis.yml
├── CHANGELOG.md
├── Cartfile.private
├── Cartfile.resolved
├── Examples/
│ └── Basic/
│ ├── AppDelegate.swift
│ ├── Assets.xcassets/
│ │ ├── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── Base.lproj/
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── Info.plist
│ ├── SceneDelegate.swift
│ ├── basic/
│ │ └── BasicChatViewController.swift
│ └── chatroom/
│ └── ChatRoomViewController.swift
├── Gemfile
├── LICENSE
├── Package.swift
├── README.md
├── RELEASING.md
├── Sources/
│ ├── Supporting Files/
│ │ ├── Info.plist
│ │ └── SwiftPhoenixClient.h
│ └── SwiftPhoenixClient/
│ ├── Channel.swift
│ ├── Defaults.swift
│ ├── Delegated.swift
│ ├── HeartbeatTimer.swift
│ ├── Message.swift
│ ├── PhoenixTransport.swift
│ ├── Presence.swift
│ ├── Push.swift
│ ├── Socket.swift
│ ├── SynchronizedArray.swift
│ └── TimeoutTimer.swift
├── SwiftPhoenixClient.podspec
├── SwiftPhoenixClient.xcodeproj/
│ ├── project.pbxproj
│ ├── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata/
│ └── xcschemes/
│ ├── Basic.xcscheme
│ ├── RxSwiftPhoenixClient.xcscheme
│ ├── StarscreamSwiftPhoenixClient.xcscheme
│ ├── SwiftPhoenixClient.xcscheme
│ └── SwiftPhoenixClientTests.xcscheme
├── Tests/
│ ├── Fakes/
│ │ ├── FakeTimerQueue.swift
│ │ ├── FakeTimerQueueSpec.swift
│ │ └── SocketSpy.swift
│ ├── Helpers/
│ │ └── TestHelpers.swift
│ ├── Info.plist
│ ├── Mocks/
│ │ ├── MockableClass.generated.swift
│ │ └── MockableProtocol.generated.swift
│ └── SwiftPhoenixClientTests/
│ ├── ChannelSpec.swift
│ ├── DefaultSerializerSpec.swift
│ ├── HeartbeatTimerSpec.swift
│ ├── MessageSpec.swift
│ ├── PresenceSpec.swift
│ ├── SocketSpec.swift
│ ├── TimeoutTimerSpec.swift
│ └── URLSessionTransportSpec.swift
├── docs/
│ ├── Classes/
│ │ ├── Channel.html
│ │ ├── Defaults.html
│ │ ├── Message.html
│ │ ├── Presence/
│ │ │ ├── Events.html
│ │ │ └── Options.html
│ │ ├── Presence.html
│ │ ├── Push.html
│ │ └── Socket.html
│ ├── Classes.html
│ ├── Enums/
│ │ └── ChannelState.html
│ ├── Enums.html
│ ├── Global Variables.html
│ ├── Protocols/
│ │ └── Serializer.html
│ ├── Protocols.html
│ ├── Structs/
│ │ ├── ChannelEvent.html
│ │ └── Delegated.html
│ ├── Structs.html
│ ├── Typealiases.html
│ ├── css/
│ │ ├── highlight.css
│ │ └── jazzy.css
│ ├── docsets/
│ │ ├── SwiftPhoenixClient.docset/
│ │ │ └── Contents/
│ │ │ ├── Info.plist
│ │ │ └── Resources/
│ │ │ ├── Documents/
│ │ │ │ ├── Classes/
│ │ │ │ │ ├── Channel.html
│ │ │ │ │ ├── Defaults.html
│ │ │ │ │ ├── Message.html
│ │ │ │ │ ├── Presence/
│ │ │ │ │ │ ├── Events.html
│ │ │ │ │ │ └── Options.html
│ │ │ │ │ ├── Presence.html
│ │ │ │ │ ├── Push.html
│ │ │ │ │ └── Socket.html
│ │ │ │ ├── Classes.html
│ │ │ │ ├── Enums/
│ │ │ │ │ └── ChannelState.html
│ │ │ │ ├── Enums.html
│ │ │ │ ├── Global Variables.html
│ │ │ │ ├── Protocols/
│ │ │ │ │ └── Serializer.html
│ │ │ │ ├── Protocols.html
│ │ │ │ ├── Structs/
│ │ │ │ │ ├── ChannelEvent.html
│ │ │ │ │ └── Delegated.html
│ │ │ │ ├── Structs.html
│ │ │ │ ├── Typealiases.html
│ │ │ │ ├── css/
│ │ │ │ │ ├── highlight.css
│ │ │ │ │ └── jazzy.css
│ │ │ │ ├── index.html
│ │ │ │ ├── js/
│ │ │ │ │ └── jazzy.js
│ │ │ │ ├── search.json
│ │ │ │ └── undocumented.json
│ │ │ └── docSet.dsidx
│ │ └── SwiftPhoenixClient.tgz
│ ├── index.html
│ ├── js/
│ │ └── jazzy.js
│ ├── search.json
│ └── undocumented.json
├── fastlane/
│ ├── Appfile
│ ├── Fastfile
│ └── README.md
└── sourcery/
├── MockableClass.stencil
├── MockableProtocol.stencil
└── MockableWebSocketClient.stencil
SYMBOL INDEX (6 symbols across 2 files)
FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/js/jazzy.js
function toggleItem (line 11) | function toggleItem($link, $content) {
function itemLinkToContent (line 17) | function itemLinkToContent($link) {
function openCurrentItemIfClosed (line 22) | function openCurrentItemIfClosed() {
FILE: docs/js/jazzy.js
function toggleItem (line 11) | function toggleItem($link, $content) {
function itemLinkToContent (line 17) | function itemLinkToContent($link) {
function openCurrentItemIfClosed (line 22) | function openCurrentItemIfClosed() {
Condensed preview — 116 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,388K chars).
[
{
"path": ".gitignore",
"chars": 358,
"preview": "# OS X\n.DS_Store\n\n# Xcode\nbuild/\n*.pbxuser\n!default.pbxuser\n*.mode1v3\n!default.mode1v3\n*.mode2v3\n!default.mode2v3\n*.pers"
},
{
"path": ".slather.yml",
"chars": 270,
"preview": "# .slather.yml\n# CodeCov\n\ncoverage_service: cobertura_xml\nxcodeproj: SwiftPhoenixClient.xcodeproj\nscheme: SwiftPhoenixCl"
},
{
"path": ".sourcery.yml",
"chars": 70,
"preview": "sources:\n - Sources/\ntemplates:\n - sourcery/\noutput:\n Tests/Mocks/\n"
},
{
"path": ".travis.yml",
"chars": 377,
"preview": "language: swift\n\nos: osx\nosx_image: xcode11.4\ncache:\n bundler: true\n directories:\n - Carthage\n\nbefore_install:\n - "
},
{
"path": "CHANGELOG.md",
"chars": 6775,
"preview": "# CHANGELOG\n\nAll notable changes to this project will be documented in this file. The format is based on [Keep a Changel"
},
{
"path": "Cartfile.private",
"chars": 61,
"preview": "github \"Quick/Quick\" ~> 4.0.0\ngithub \"Quick/Nimble\" ~> 9.0.0\n"
},
{
"path": "Cartfile.resolved",
"chars": 61,
"preview": "github \"Quick/Nimble\" \"v9.2.1\"\ngithub \"Quick/Quick\" \"v4.0.0\"\n"
},
{
"path": "Examples/Basic/AppDelegate.swift",
"chars": 421,
"preview": "//\n// AppDelegate.swift\n// Basic\n//\n// Created by Daniel Rees on 10/23/20.\n// Copyright © 2021 SwiftPhoenixClient. A"
},
{
"path": "Examples/Basic/Assets.xcassets/AppIcon.appiconset/Contents.json",
"chars": 1591,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"iphone\",\n \"scale\" : \"2x\",\n \"size\" : \"20x20\"\n },\n {\n \"idiom\""
},
{
"path": "Examples/Basic/Assets.xcassets/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "Examples/Basic/Base.lproj/LaunchScreen.storyboard",
"chars": 1665,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard"
},
{
"path": "Examples/Basic/Base.lproj/Main.storyboard",
"chars": 26376,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3"
},
{
"path": "Examples/Basic/Info.plist",
"chars": 2103,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Examples/Basic/SceneDelegate.swift",
"chars": 245,
"preview": "//\n// SceneDelegate.swift\n// Basic\n//\n// Created by Daniel Rees on 10/23/20.\n// Copyright © 2021 SwiftPhoenixClient."
},
{
"path": "Examples/Basic/basic/BasicChatViewController.swift",
"chars": 5021,
"preview": "//\n// BasicChatViewController.swift\n// Basic\n//\n// Created by Daniel Rees on 10/23/20.\n// Copyright © 2021 SwiftPhoe"
},
{
"path": "Examples/Basic/chatroom/ChatRoomViewController.swift",
"chars": 6919,
"preview": "//\n// ChatRoomViewController.swift\n// Basic\n//\n// Created by Daniel Rees on 12/22/20.\n// Copyright © 2021 SwiftPhoen"
},
{
"path": "Gemfile",
"chars": 60,
"preview": "source \"https://rubygems.org\"\n\ngem 'fastlane'\ngem 'slather'\n"
},
{
"path": "LICENSE",
"chars": 1078,
"preview": "Copyright (c) 2015 David Stump <david@davidstump.net>\n\nPermission is hereby granted, free of charge, to any person obtai"
},
{
"path": "Package.swift",
"chars": 1105,
"preview": "// swift-tools-version:5.3\n// The swift-tools-version declares the minimum version of Swift required to build this packa"
},
{
"path": "README.md",
"chars": 4227,
"preview": "# Swift Phoenix Client\n\n[](https://swift.org/)\n[!["
},
{
"path": "RELEASING.md",
"chars": 566,
"preview": "Release Process\n===============\n\n 1. Ensure `version` in `SwiftPhoenixClient.podsec` is set to the version you want to "
},
{
"path": "Sources/Supporting Files/Info.plist",
"chars": 865,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Sources/Supporting Files/SwiftPhoenixClient.h",
"chars": 574,
"preview": "//\n// SwiftPhoenixClient.h\n// SwiftPhoenixClient\n//\n// Created by Daniel Rees on 10/21/20.\n// Copyright © 2021 Swift"
},
{
"path": "Sources/SwiftPhoenixClient/Channel.swift",
"chars": 23138,
"preview": "// Copyright (c) 2021 David Stump <david@davidstump.net>\n//\n// Permission is hereby granted, free of charge, to any pers"
},
{
"path": "Sources/SwiftPhoenixClient/Defaults.swift",
"chars": 3623,
"preview": "// Copyright (c) 2021 David Stump <david@davidstump.net>\n//\n// Permission is hereby granted, free of charge, to any pers"
},
{
"path": "Sources/SwiftPhoenixClient/Delegated.swift",
"chars": 3449,
"preview": "// Copyright (c) 2021 David Stump <david@davidstump.net>\n//\n// Permission is hereby granted, free of charge, to any pers"
},
{
"path": "Sources/SwiftPhoenixClient/HeartbeatTimer.swift",
"chars": 4802,
"preview": "// Copyright (c) 2021 David Stump <david@davidstump.net>\n//\n// Permission is hereby granted, free of charge, to any pers"
},
{
"path": "Sources/SwiftPhoenixClient/Message.swift",
"chars": 2691,
"preview": "// Copyright (c) 2021 David Stump <david@davidstump.net>\n//\n// Permission is hereby granted, free of charge, to any pers"
},
{
"path": "Sources/SwiftPhoenixClient/PhoenixTransport.swift",
"chars": 11056,
"preview": "// Copyright (c) 2021 David Stump <david@davidstump.net>\n//\n// Permission is hereby granted, free of charge, to any pers"
},
{
"path": "Sources/SwiftPhoenixClient/Presence.swift",
"chars": 14337,
"preview": "// Copyright (c) 2021 David Stump <david@davidstump.net>\n//\n// Permission is hereby granted, free of charge, to any pers"
},
{
"path": "Sources/SwiftPhoenixClient/Push.swift",
"chars": 9182,
"preview": "// Copyright (c) 2021 David Stump <david@davidstump.net>\n//\n// Permission is hereby granted, free of charge, to any pers"
},
{
"path": "Sources/SwiftPhoenixClient/Socket.swift",
"chars": 34799,
"preview": "// Copyright (c) 2021 David Stump <david@davidstump.net>\n//\n// Permission is hereby granted, free of charge, to any pers"
},
{
"path": "Sources/SwiftPhoenixClient/SynchronizedArray.swift",
"chars": 1234,
"preview": "//\n// SynchronizedArray.swift\n// SwiftPhoenixClient\n//\n// Created by Daniel Rees on 4/12/23.\n// Copyright © 2023 Swi"
},
{
"path": "Sources/SwiftPhoenixClient/TimeoutTimer.swift",
"chars": 4080,
"preview": "// Copyright (c) 2021 David Stump <david@davidstump.net>\n//\n// Permission is hereby granted, free of charge, to any pers"
},
{
"path": "SwiftPhoenixClient.podspec",
"chars": 1509,
"preview": "#\n# Be sure to run `pod lib lint SwiftPhoenixClient.podspec' to ensure this is a\n# valid spec before submitting.\n#\n# Any"
},
{
"path": "SwiftPhoenixClient.xcodeproj/project.pbxproj",
"chars": 41818,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 52;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "SwiftPhoenixClient.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 163,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:SwiftPhoenixCli"
},
{
"path": "SwiftPhoenixClient.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
"chars": 238,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "SwiftPhoenixClient.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
"chars": 181,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "SwiftPhoenixClient.xcodeproj/xcshareddata/xcschemes/Basic.xcscheme",
"chars": 2861,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"1420\"\n version = \"1.3\">\n <BuildAction\n "
},
{
"path": "SwiftPhoenixClient.xcodeproj/xcshareddata/xcschemes/RxSwiftPhoenixClient.xcscheme",
"chars": 2455,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"1420\"\n version = \"1.3\">\n <BuildAction\n "
},
{
"path": "SwiftPhoenixClient.xcodeproj/xcshareddata/xcschemes/StarscreamSwiftPhoenixClient.xcscheme",
"chars": 2487,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"1420\"\n version = \"1.3\">\n <BuildAction\n "
},
{
"path": "SwiftPhoenixClient.xcodeproj/xcshareddata/xcschemes/SwiftPhoenixClient.xcscheme",
"chars": 2910,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"1420\"\n version = \"1.3\">\n <BuildAction\n "
},
{
"path": "SwiftPhoenixClient.xcodeproj/xcshareddata/xcschemes/SwiftPhoenixClientTests.xcscheme",
"chars": 1846,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"1420\"\n version = \"1.3\">\n <BuildAction\n "
},
{
"path": "Tests/Fakes/FakeTimerQueue.swift",
"chars": 2971,
"preview": "// Copyright (c) 2021 David Stump <david@davidstump.net>\n//\n// Permission is hereby granted, free of charge, to any pers"
},
{
"path": "Tests/Fakes/FakeTimerQueueSpec.swift",
"chars": 5466,
"preview": "// Copyright (c) 2021 David Stump <david@davidstump.net>\n//\n// Permission is hereby granted, free of charge, to any pers"
},
{
"path": "Tests/Fakes/SocketSpy.swift",
"chars": 1927,
"preview": "// Copyright (c) 2021 David Stump <david@davidstump.net>\n//\n// Permission is hereby granted, free of charge, to any pers"
},
{
"path": "Tests/Helpers/TestHelpers.swift",
"chars": 1867,
"preview": "// Copyright (c) 2021 David Stump <david@davidstump.net>\n//\n// Permission is hereby granted, free of charge, to any pers"
},
{
"path": "Tests/Info.plist",
"chars": 680,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Tests/Mocks/MockableClass.generated.swift",
"chars": 41497,
"preview": "// Generated using Sourcery 1.0.2 — https://github.com/krzysztofzablocki/Sourcery\n// DO NOT EDIT\n\n// swiftlint:disable l"
},
{
"path": "Tests/Mocks/MockableProtocol.generated.swift",
"chars": 2024,
"preview": "// Generated using Sourcery 1.0.2 — https://github.com/krzysztofzablocki/Sourcery\n// DO NOT EDIT\n\n// swiftlint:disable l"
},
{
"path": "Tests/SwiftPhoenixClientTests/ChannelSpec.swift",
"chars": 35230,
"preview": "//\n// ChannelSpec.swift\n// SwiftPhoenixClient\n//\n// Created by Daniel Rees on 5/18/18.\n//\n\nimport Quick\nimport Nimble"
},
{
"path": "Tests/SwiftPhoenixClientTests/DefaultSerializerSpec.swift",
"chars": 1023,
"preview": "//\n// DefaultSerializerSpec.swift\n// SwiftPhoenixClient\n//\n// Created by Daniel Rees on 1/17/19.\n//\n\nimport Quick\nimp"
},
{
"path": "Tests/SwiftPhoenixClientTests/HeartbeatTimerSpec.swift",
"chars": 1824,
"preview": "//\n// HeartbeatTimerSpec.swift\n// SwiftPhoenixClientTests\n//\n// Created by Daniel Rees on 8/24/21.\n// Copyright © 20"
},
{
"path": "Tests/SwiftPhoenixClientTests/MessageSpec.swift",
"chars": 1582,
"preview": "//\n// MessageSpec.swift\n// SwiftPhoenixClientTests\n//\n// Created by Daniel Rees on 10/27/21.\n// Copyright © 2021 Swi"
},
{
"path": "Tests/SwiftPhoenixClientTests/PresenceSpec.swift",
"chars": 17118,
"preview": "//\n// PresenceSpec.swift\n// SwiftPhoenixClient\n//\n// Created by Simon Bergström on 2018-10-03.\n//\n\nimport Quick\nimpor"
},
{
"path": "Tests/SwiftPhoenixClientTests/SocketSpec.swift",
"chars": 33715,
"preview": "//\n// SocketSpec.swift\n// SwiftPhoenixClient\n//\n// Created by Daniel Rees on 2/10/18.\n//\n\nimport Quick\nimport Nimble\n"
},
{
"path": "Tests/SwiftPhoenixClientTests/TimeoutTimerSpec.swift",
"chars": 2254,
"preview": "//\n// TimeoutTimerSpec.swift\n// SwiftPhoenixClientTests\n//\n// Created by Daniel Rees on 2/10/19.\n//\n\nimport Quick\nimp"
},
{
"path": "Tests/SwiftPhoenixClientTests/URLSessionTransportSpec.swift",
"chars": 1991,
"preview": "//\n// URLSessionTransportSpec.swift\n// SwiftPhoenixClientTests\n//\n// Created by Daniel Rees on 4/1/21.\n// Copyright "
},
{
"path": "docs/Classes/Channel.html",
"chars": 64735,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Channel Class Reference</title>\n <link rel=\"stylesheet\" type=\"te"
},
{
"path": "docs/Classes/Defaults.html",
"chars": 13746,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Defaults Class Reference</title>\n <link rel=\"stylesheet\" type=\"t"
},
{
"path": "docs/Classes/Message.html",
"chars": 11470,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Message Class Reference</title>\n <link rel=\"stylesheet\" type=\"te"
},
{
"path": "docs/Classes/Presence/Events.html",
"chars": 7246,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Events Enumeration Reference</title>\n <link rel=\"stylesheet\" typ"
},
{
"path": "docs/Classes/Presence/Options.html",
"chars": 7163,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Options Structure Reference</title>\n <link rel=\"stylesheet\" type"
},
{
"path": "docs/Classes/Presence.html",
"chars": 62050,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Presence Class Reference</title>\n <link rel=\"stylesheet\" type=\"t"
},
{
"path": "docs/Classes/Push.html",
"chars": 23458,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Push Class Reference</title>\n <link rel=\"stylesheet\" type=\"text/"
},
{
"path": "docs/Classes/Socket.html",
"chars": 86165,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Socket Class Reference</title>\n <link rel=\"stylesheet\" type=\"tex"
},
{
"path": "docs/Classes.html",
"chars": 21718,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Classes Reference</title>\n <link rel=\"stylesheet\" type=\"text/cs"
},
{
"path": "docs/Enums/ChannelState.html",
"chars": 11114,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>ChannelState Enumeration Reference</title>\n <link rel=\"styleshee"
},
{
"path": "docs/Enums.html",
"chars": 5730,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Enumerations Reference</title>\n <link rel=\"stylesheet\" type=\"te"
},
{
"path": "docs/Global Variables.html",
"chars": 10505,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Global Variables Reference</title>\n <link rel=\"stylesheet\" type"
},
{
"path": "docs/Protocols/Serializer.html",
"chars": 8648,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Serializer Protocol Reference</title>\n <link rel=\"stylesheet\" ty"
},
{
"path": "docs/Protocols.html",
"chars": 6401,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Protocols Reference</title>\n <link rel=\"stylesheet\" type=\"text/"
},
{
"path": "docs/Structs/ChannelEvent.html",
"chars": 12663,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>ChannelEvent Structure Reference</title>\n <link rel=\"stylesheet\""
},
{
"path": "docs/Structs/Delegated.html",
"chars": 26436,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Delegated Structure Reference</title>\n <link rel=\"stylesheet\" ty"
},
{
"path": "docs/Structs.html",
"chars": 7433,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Structures Reference</title>\n <link rel=\"stylesheet\" type=\"text"
},
{
"path": "docs/Typealiases.html",
"chars": 7187,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Type Aliases Reference</title>\n <link rel=\"stylesheet\" type=\"te"
},
{
"path": "docs/css/highlight.css",
"chars": 4479,
"preview": "/* Credit to https://gist.github.com/wataru420/2048287 */\n.highlight {\n /* Comment */\n /* Error */\n /* Keyword */\n /"
},
{
"path": "docs/css/jazzy.css",
"chars": 7602,
"preview": "html, body, div, span, h1, h3, h4, p, a, code, em, img, ul, li, table, tbody, tr, td {\n background: transparent;\n bord"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Info.plist",
"chars": 647,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Classes/Channel.html",
"chars": 64735,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Channel Class Reference</title>\n <link rel=\"stylesheet\" type=\"te"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Classes/Defaults.html",
"chars": 13746,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Defaults Class Reference</title>\n <link rel=\"stylesheet\" type=\"t"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Classes/Message.html",
"chars": 11470,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Message Class Reference</title>\n <link rel=\"stylesheet\" type=\"te"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Classes/Presence/Events.html",
"chars": 7246,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Events Enumeration Reference</title>\n <link rel=\"stylesheet\" typ"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Classes/Presence/Options.html",
"chars": 7163,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Options Structure Reference</title>\n <link rel=\"stylesheet\" type"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Classes/Presence.html",
"chars": 62050,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Presence Class Reference</title>\n <link rel=\"stylesheet\" type=\"t"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Classes/Push.html",
"chars": 23458,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Push Class Reference</title>\n <link rel=\"stylesheet\" type=\"text/"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Classes/Socket.html",
"chars": 86165,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Socket Class Reference</title>\n <link rel=\"stylesheet\" type=\"tex"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Classes.html",
"chars": 21718,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Classes Reference</title>\n <link rel=\"stylesheet\" type=\"text/cs"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Enums/ChannelState.html",
"chars": 11114,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>ChannelState Enumeration Reference</title>\n <link rel=\"styleshee"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Enums.html",
"chars": 5730,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Enumerations Reference</title>\n <link rel=\"stylesheet\" type=\"te"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Global Variables.html",
"chars": 10505,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Global Variables Reference</title>\n <link rel=\"stylesheet\" type"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Protocols/Serializer.html",
"chars": 8648,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Serializer Protocol Reference</title>\n <link rel=\"stylesheet\" ty"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Protocols.html",
"chars": 6401,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Protocols Reference</title>\n <link rel=\"stylesheet\" type=\"text/"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Structs/ChannelEvent.html",
"chars": 12663,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>ChannelEvent Structure Reference</title>\n <link rel=\"stylesheet\""
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Structs/Delegated.html",
"chars": 26436,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Delegated Structure Reference</title>\n <link rel=\"stylesheet\" ty"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Structs.html",
"chars": 7433,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Structures Reference</title>\n <link rel=\"stylesheet\" type=\"text"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Typealiases.html",
"chars": 7187,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Type Aliases Reference</title>\n <link rel=\"stylesheet\" type=\"te"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/css/highlight.css",
"chars": 4479,
"preview": "/* Credit to https://gist.github.com/wataru420/2048287 */\n.highlight {\n /* Comment */\n /* Error */\n /* Keyword */\n /"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/css/jazzy.css",
"chars": 7602,
"preview": "html, body, div, span, h1, h3, h4, p, a, code, em, img, ul, li, table, tbody, tr, td {\n background: transparent;\n bord"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/index.html",
"chars": 8623,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>SwiftPhoenixClient Reference</title>\n <link rel=\"stylesheet\" ty"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/js/jazzy.js",
"chars": 1507,
"preview": "window.jazzy = {'docset': false}\nif (typeof window.dash != 'undefined') {\n document.documentElement.className += ' dash"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/search.json",
"chars": 26305,
"preview": "{\"Typealiases.html#/s:18SwiftPhoenixClient7Payloada\":{\"name\":\"Payload\",\"abstract\":\"<p>Alias for a JSON dictionary [Strin"
},
{
"path": "docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/undocumented.json",
"chars": 10428,
"preview": "{\n \"warnings\": [\n {\n \"file\": \"/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Presence.swift\""
},
{
"path": "docs/index.html",
"chars": 8623,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>SwiftPhoenixClient Reference</title>\n <link rel=\"stylesheet\" ty"
},
{
"path": "docs/js/jazzy.js",
"chars": 1507,
"preview": "window.jazzy = {'docset': false}\nif (typeof window.dash != 'undefined') {\n document.documentElement.className += ' dash"
},
{
"path": "docs/search.json",
"chars": 26305,
"preview": "{\"Typealiases.html#/s:18SwiftPhoenixClient7Payloada\":{\"name\":\"Payload\",\"abstract\":\"<p>Alias for a JSON dictionary [Strin"
},
{
"path": "docs/undocumented.json",
"chars": 10914,
"preview": "{\n \"warnings\": [\n {\n \"file\": \"/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Channel.swift\",\n "
},
{
"path": "fastlane/Appfile",
"chars": 228,
"preview": "# app_identifier \"[[APP_IDENTIFIER]]\" # The bundle identifier of your app\n# apple_id \"[[APPLE_ID]]\" # Your Apple email a"
},
{
"path": "fastlane/Fastfile",
"chars": 726,
"preview": "# This file contains the fastlane.tools configuration\n# You can find the documentation at https://docs.fastlane.tools\n#\n"
},
{
"path": "fastlane/README.md",
"chars": 688,
"preview": "fastlane documentation\n================\n# Installation\n\nMake sure you have the latest version of the Xcode command line "
},
{
"path": "sourcery/MockableClass.stencil",
"chars": 7388,
"preview": "// swiftlint:disable line_length\n// swiftlint:disable variable_name\n\n@testable import SwiftPhoenixClient\n\n{% macro swift"
},
{
"path": "sourcery/MockableProtocol.stencil",
"chars": 7088,
"preview": "// swiftlint:disable line_length\n// swiftlint:disable variable_name\n\nimport Foundation\n#if os(iOS) || os(tvOS) || os(wat"
},
{
"path": "sourcery/MockableWebSocketClient.stencil",
"chars": 5765,
"preview": "// swiftlint:disable line_length\n// swiftlint:disable variable_name\n\nimport Starscream\n@testable import SwiftPhoenixClie"
}
]
// ... and 2 more files (download for full content)
About this extraction
This page contains the full source code of the davidstump/SwiftPhoenixClient GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 116 files (1.2 MB), approximately 321.2k tokens, and a symbol index with 6 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.