Skip to content

Commit afc7cbe

Browse files
committed
⚡️Implement WebSockets support 🎉
1 parent 1a871d5 commit afc7cbe

15 files changed

+320
-70
lines changed

CodyFire/Classes/CodyFire.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ open class CodyFire {
5050
public var dateDecodingStrategy: DateCodingStrategy?
5151
public var dateEncodingStrategy: DateCodingStrategy?
5252

53-
public let ws = WS()
53+
public var ws: WS { return WS.shared }
5454

5555
var customErrors: [NetworkError] = [
5656
NetworkError(code: .unauthorized, description: "You're not authorized"),

CodyFire/Classes/CodyFireEnvironment.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ public struct CodyFireEnvironment {
3232
_wsURL = wsURL
3333
}
3434

35-
public init(baseURL: String, path: String? = nil) {
35+
public init(baseURL: String, path: String? = nil, wsPath: String? = nil) {
3636
_apiURL = ServerURL(base: baseURL, path: path)
37+
_wsURL = ServerURL(base: baseURL, path: wsPath ?? path)
3738
}
3839

3940
public var apiBaseURL: String {

CodyFire/Classes/Logger.swift

+9
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ func log(_ level: LogLevel, _ message: String) {
1818
CodyFire.shared.logHandler?(level, message)
1919
}
2020

21+
func wslog(_ level: LogLevel, _ message: String) {
22+
#if DEBUG
23+
if level.rawValue <= CodyFire.shared.logLevel.rawValue {
24+
print("🌸 [CodyFire]⚡️[WS]: " + message)
25+
}
26+
#endif
27+
CodyFire.shared.logHandler?(level, message)
28+
}
29+
2130
public enum LogLevel: Int {
2231
///Don't log anything at all.
2332
case off
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//
2+
// NotificationCenter+EventIdentifier.swift
3+
// CodyFire
4+
//
5+
// Created by Mihael Isaev on 19/02/2019.
6+
//
7+
8+
import Foundation
9+
10+
extension NotificationCenter {
11+
public func addObserver<Model: WSEventModel>(forEvent event: WSEventIdentifier<Model>, using: @escaping (Model) -> Void) {
12+
addObserver(forName: event.notification, object: nil, queue: nil) { notification in
13+
if let payload = notification.object as? Model {
14+
using(payload)
15+
}
16+
}
17+
}
18+
19+
func post<Model: WSEventModel>(forEvent event: WSEventIdentifier<Model>, object anObject: Model) {
20+
post(name: event.notification, object: anObject)
21+
}
22+
}
23+
24+
extension WSEventIdentifier {
25+
fileprivate var notification: NSNotification.Name {
26+
return NSNotification.Name(rawValue: "ws.event." + self.uid)
27+
}
28+
}

CodyFire/Classes/WS/WS+Delegate.swift

-53
This file was deleted.

CodyFire/Classes/WS/WS+Emit.swift

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//
2+
// WS+Emit.swift
3+
// CodyFire
4+
//
5+
// Created by Mihael Isaev on 19/02/2019.
6+
//
7+
8+
import Foundation
9+
import Starscream
10+
11+
extension WS {
12+
public func emit(text: String, completion: (() -> Void)? = nil) {
13+
socket?.write(string: text, completion: completion)
14+
}
15+
16+
public func emit(data: Data, completion: (() -> Void)? = nil) {
17+
socket?.write(data: data, completion: completion)
18+
}
19+
20+
public func emit<Model: WSEventModel>(text event: WSEventIdentifier<Model>, payload: Model, completion: (() -> Void)? = nil) {
21+
do {
22+
let data = try prepareData(with: payload)
23+
guard let text = String(data: data, encoding: .utf8) else {
24+
throw WSExpectationError(reason: "Unable to encode model payload Data into String")
25+
}
26+
socket?.write(string: text, completion: completion)
27+
} catch {
28+
wslog(.error, String(describing: error))
29+
}
30+
}
31+
32+
public func emit<Model: WSEventModel>(data event: WSEventIdentifier<Model>, payload: Model, completion: (() -> Void)? = nil) {
33+
do {
34+
socket?.write(data: try prepareData(with: payload), completion: completion)
35+
} catch {
36+
wslog(.error, String(describing: error))
37+
}
38+
}
39+
40+
private func prepareData<Model: WSEventModel>(with payload: Model) throws -> Data {
41+
let jsonEncoder = JSONEncoder()
42+
var dateEncodingStrategy = CodyFire.shared.dateEncodingStrategy ?? DateCodingStrategy.default
43+
if let payload = payload as? CustomDateEncodingStrategy {
44+
dateEncodingStrategy = payload.dateEncodingStrategy
45+
}
46+
return try jsonEncoder.encode(payload)
47+
}
48+
}

CodyFire/Classes/WS/WS.swift

+16-15
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@
88
import Foundation
99
import Starscream
1010

11-
public protocol WSDelegate {
12-
func wsConnecting()
13-
func wsConnected()
14-
func wsDisconnected()
15-
}
11+
private var _sharedInstance = WS()
1612

17-
public class WS: WebSocketDelegate {
13+
public class WS {
14+
public class var shared: WS {
15+
return _sharedInstance
16+
}
17+
1818
var socket: WebSocket?
19-
var delegate: WSDelegate?
19+
public var delegate: WSObserver?
2020

2121
var reconnect = true
2222

@@ -27,23 +27,24 @@ public class WS: WebSocketDelegate {
2727
disconnect()
2828
socket = nil
2929
reconnect = true
30-
guard let url = URL(string: CodyFire.shared.wsURL) else { return } //TODO: throw
30+
guard let url = URL(string: CodyFire.shared.wsURL) else { throw WSExpectationError(reason: "WS url is nil") }
3131
var request = URLRequest(url: url)
3232
request.timeoutInterval = 5
33-
CodyFire.shared.fillHeaders?().forEach { v in
34-
request.setValue(v.value, forHTTPHeaderField: v.key)
33+
CodyFire.shared.globalHeaders.forEach { key, value in
34+
request.setValue(value, forHTTPHeaderField: key)
3535
}
3636
socket = WebSocket(request: request)
37-
log(.info, "ws preparing to connect: \(request)")
38-
socket?.delegate = self
39-
delegate?.wsConnecting()
37+
wslog(.info, "preparing to connect: \(request)")
38+
socket?.delegate = delegate
39+
// TODO: delegate?.connecting() ?
4040
socket?.connect()
4141
}
4242

4343
public func disconnect() {
44-
log(.info, "ws disconnected")
4544
reconnect = false
4645
socket?.disconnect(forceTimeout: 0.1, closeCode: 1000)
47-
delegate?.wsDisconnected()
46+
if let socket = socket {
47+
delegate?.websocketDidDisconnect(socket: socket, error: nil)
48+
}
4849
}
4950
}
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//
2+
// WSAnyEventModel.swift
3+
// CodyFire
4+
//
5+
// Created by Mihael Isaev on 19/02/2019.
6+
//
7+
8+
public protocol WSAnyEventModel: Codable {
9+
typealias EventKey = KeyPath<Self, String>
10+
static var eventKey: EventKey { get }
11+
}
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//
2+
// WSBindController.swift
3+
// CodyFire
4+
//
5+
// Created by Mihael Isaev on 19/02/2019.
6+
//
7+
8+
import Foundation
9+
import Starscream
10+
11+
open class WSBindController<EventPrototype: WSAnyEventModel>: WSObserver {
12+
public typealias Default = WSBindController<_WSDefaultEventModel>
13+
14+
var handlers: [String: (WebSocketClient, Data) -> Void] = [:]
15+
16+
public override init () {
17+
super.init()
18+
}
19+
20+
public func bind<Model: WSEventModel>(_ identifier: WSEventIdentifier<Model>, handler: ((WebSocketClient, Model) -> Void)? = nil) {
21+
handlers[Model.key] = { client, data in
22+
do {
23+
let decoder = JSONDecoder()
24+
decoder.dateDecodingStrategy = CodyFire.shared.dateDecodingStrategy?.jsonDateDecodingStrategy
25+
?? DateCodingStrategy.default.jsonDateDecodingStrategy
26+
let model = try decoder.decode(Model.self, from: data)
27+
handler?(client, model)
28+
NotificationCenter.default.post(forEvent: identifier, object: model)
29+
} catch {
30+
wslog(.error, "\(error)")
31+
}
32+
}
33+
}
34+
35+
public override func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
36+
guard let data = text.data(using: .utf8) else { return }
37+
decodeAndCallHandler(socket: socket, data: data)
38+
}
39+
40+
public override func websocketDidReceiveData(socket: WebSocketClient, data: Data) {
41+
decodeAndCallHandler(socket: socket, data: data)
42+
}
43+
44+
func decodeAndCallHandler(socket: WebSocketClient, data: Data) {
45+
wslog(.debug, String(data: data, encoding: .utf8) ?? "unable to convert data to string")
46+
do {
47+
let decoded = try JSONDecoder().decode(EventPrototype.self, from: data)
48+
let key = decoded[keyPath: EventPrototype.eventKey]
49+
guard let handler = handlers[key] else {
50+
throw WSExpectationError(reason: "event.key `\(key)` hasn't been found in handlers array")
51+
}
52+
handler(socket, data)
53+
} catch {
54+
wslog(.error, "\(error)")
55+
}
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// WSDefaultEventModel.swift
3+
// CodyFire
4+
//
5+
// Created by Mihael Isaev on 19/02/2019.
6+
//
7+
8+
public struct _WSDefaultEventModel: WSAnyEventModel {
9+
public static var eventKey: EventKey { return \.event }
10+
public var event: String
11+
}
12+
13+
public protocol WSDefaultEventModel: WSEventModel {
14+
/// This model's unique identifier.
15+
var event: String { get set }
16+
}
17+
18+
extension WSDefaultEventModel {
19+
/// See `WSEventModel`.
20+
public static var eventKey: EventKey { return \.event }
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// WSEventIdentifier.swift
3+
// CodyFire
4+
//
5+
// Created by Mihael Isaev on 19/02/2019.
6+
//
7+
8+
public protocol WSEventIdentifierProtocol: Hashable, CustomStringConvertible, Codable {
9+
associatedtype Event: WSEventModel
10+
}
11+
12+
public struct WSEventIdentifier<E: WSEventModel>: WSEventIdentifierProtocol {
13+
public typealias Event = E
14+
15+
/// The unique id.
16+
public var uid: String {
17+
return Event.key
18+
}
19+
20+
/// See `CustomStringConvertible`.
21+
public var description: String {
22+
return uid
23+
}
24+
25+
public init () {}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//
2+
// WSExpectationError.swift
3+
// CodyFire
4+
//
5+
// Created by Mihael Isaev on 19/02/2019.
6+
//
7+
8+
public protocol WSExpectationErrorProtocol: Error {
9+
var reason: String { get }
10+
}
11+
12+
public struct WSExpectationError: WSExpectationErrorProtocol {
13+
public var reason: String
14+
}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//
2+
// WSEventModel.swift
3+
// CodyFire
4+
//
5+
// Created by Mihael Isaev on 19/02/2019.
6+
//
7+
8+
public protocol WSEventModel: WSAnyEventModel {
9+
static var key: String { get }
10+
}

0 commit comments

Comments
 (0)