Releases: Aldo10012/EZNetworking
5.4.0
What's new?
Both of my WebSocketClient and ServerSentEventClient are entirely Swift Concurrency-based, with no support for callbacks or publishers. In order to support callbacks and publishers for any user who is still relying on the old paradigms, I created adaptors.
Websocket adaptors
Callback adaptor
/// init
let ws = WebSocketCallbackAdapter(url: "some_websocket_url")
/// connect
ws.connect { result in
// handle result
}
/// disconnect
ws.disconnect { result in
// handle result
}
/// terminate
ws.terminate()
/// send message
ws.send(.string("Hello")) { result in
// handle result
}
/// receive messages
ws.onMessage { result in
// handle result
}
/// state change
ws.onStateChange { result in
// handle result
}Publisher adaptor
let ws = WebSocketPublisherAdapter(url: "some_websocket_url")
/// connect
ws.connect()
.sink(
// handle sink
)
.store(in: &cancellables)
/// disconnect
ws.disconnect()
.sink(
// handle sink
)
.store(in: &cancellables)
/// terminate
ws.terminate()
/// send message
ws.send(.string("Hello"))
.sink(
// handle sink
)
.store(in: &cancellables)
/// receive messages
ws.messages
.sink(
// handle sink
)
.store(in: &cancellables)
/// state change
ws.stateEvents
.sink(
// handle sink
)
.store(in: &cancellables)ServerSentEvent adaptors
Callback adaptor
/// init
let sse = ServerSentEventCallbackAdapter(url: "some_websocket_url")
/// connect
sse.connect { result in
// handle result
}
/// disconnect
sse.disconnect { result in
// handle result
}
/// terminate
sse.terminate()
/// receive event
ws.onEvent { result in
// handle result
}
/// state change
ws.onStateChange { result in
// handle result
}Publisher adaptor
let sse = ServerSentEventPublisherAdapter(url: "some_websocket_url")
/// connect
sse.connect()
.sink(
// handle sink
)
.store(in: &cancellables)
/// disconnect
sse.disconnect()
.sink(
// handle sink
)
.store(in: &cancellables)
/// terminate
sse.terminate()
/// receive events
sse.onEvent
.sink(
// handle sink
)
.store(in: &cancellables)
/// state change
ws.stateEvents
.sink(
// handle sink
)
.store(in: &cancellables)5.3.3
What's new?
- Update Error Handling
- Update Response Validation
Updated Error Handling
Replace WebSocketError with NetworkingError.webSocketFailure(reason: _)
Replace SSEError with NetworkingError.serverSentEventFailure(reason: _)
Updated Response Validation
Rename ResponseValidatorImpl to DefaultResponseValidator
Also added an ooptional init argument for expected Response Headers
Example: DefaultResponseValidator(expectedHttpHeaders: [.contentType(.eventStream)]
5.3.2
5.3.1
5.3.0
What's new?
Adding support for Server-Sent Events (SSE)
Server-Sent Events enable real-time, unidirectional streaming from server to client over standard HTTP—perfect for live feeds, dashboards, notifications, and any scenario where only the server needs to push updates.
What are Server-Sent Events?
Server-Sent Events (SSE) is a standard protocol that allows servers to push real-time updates to clients over a persistent HTTP connection. Unlike WebSockets, which provide bidirectional communication, SSE is optimized for one-way data flow from server to client.
How it works:
- Client establishes a long-lived HTTP connection with Content-Type: text/event-stream
- Server keeps the connection open and pushes events whenever new data is available
- Client receives events as a continuous stream without making repeated requests
Key characteristics:
- Unidirectional: Server pushes to client (client doesn't send messages back)
- Built on HTTP: No protocol upgrade—works over standard HTTP/HTTPS
- Automatic reconnection: Built-in reconnection with exponential backoff
- Event IDs: Resume streams from where you left off (no duplicates or gaps)
- Lightweight: Simple text-based protocol, lower overhead than WebSockets
Common Use Cases
Live dashboards — Real-time metrics and monitoring
Stock/crypto tickers — Continuous price updates
News feeds — Push notifications for breaking news
Social media updates — New posts, likes, comments
Sports scores — Live game updates
Server monitoring — Log streaming and status updates
Read more about SSE: MDN Web Docs - Server-sent events
How to use?
How to initialize
let request = SSERequest(
url: "https://api.example.com/events",
additionalheaders: [
.authorization(.bearer("your-token")),
.customHeader(name: "X-Client-ID", value: "ios-app")
]
)
let manager = ServerSentEventManager(request: request)
// Optional: Configure automatic reconnection
let reconnectionConfig = SSEReconnectionConfig(
enabled: true,
maxAttempts: 5, // nil for unlimited
initialDelay: 1.0, // seconds
maxDelay: 60.0, // seconds
backoffMultiplier: 2.0 // exponential backoff
)
let manager = ServerSentEventManager(
url: "https://api.example.com/events",
reconnectionConfig: reconnectionConfig
)How to connect
try await manager.connect()How to disconnect
try await manager.disconnect()How to terminate
try await manager.terminate()How to Receive Events
for await event in manager.events {
print("Received:", event.data)
// Access event fields
if let id = event.id {
print("Event ID:", id)
}
if let type = event.event {
print("Event type:", type)
}
if let retry = event.retry {
print("Server retry interval:", retry, "ms")
}
}How to handle different event types
for await event in manager.events {
guard let data = event.data.data(using: .utf8) else { return }
switch event.event {
case "price_update":
let price = try? JSONDecoder().decode(StockPrice.self, from: data)
updateUI(with: price)
case "notification":
let notification = try? JSONDecoder().decode(Notification.self, from: data)
showAlert(notification)
default:
// Handle default "message" type
print("Message:", event.data)
}
}How to Observe Connection State
for await state in manager.stateEvents {
switch state {
case .notConnected:
// Initial state before connecting
statusLabel.text = "Offline"
case .connecting:
// Connection attempt in progress
statusLabel.text = "Connecting..."
case .connected:
// Successfully connected and streaming
statusLabel.text = "Live"
case .disconnected(let reason):
// Connection lost
switch reason {
case .streamEnded, .streamError:
// Automatic reconnection will occur if configured
statusLabel.text = "Reconnecting..."
case .manuallyDisconnected:
statusLabel.text = "Offline"
case .terminated:
statusLabel.text = "Disconnected"
}
}
}5.2.0
What's new?
Updated NetworkingError enum.
public enum NetworkingError: Error {
case couldNotBuildURLRequest(reason: URLBuildFailureReason)
case decodingFailed(reason: DecodingFailureReason)
case responseValidationFailed(reason: ResponseValidationFailureReason)
case requestFailed(reason: RequestFailureReason)
}
public enum DecodingFailureReason: Equatable, Sendable {
case decodingError(underlying: DecodingError)
case other(underlying: Error)
}
public enum RequestFailureReason: Equatable, Sendable {
case urlError(underlying: URLError)
case unknownError(underlying: Error)
}
public enum ResponseValidationFailureReason: Equatable, Sendable {
case noHTTPURLResponse
case badHTTPResponse(underlying: HTTPResponse)
}
public enum URLBuildFailureReason: Equatable, Sendable {
case noURL
case invalidURL
case invalidScheme(String?)
case missingHost
}Error handling example
do {
let response = try await AsyncRequestPerformer().perform(request: request, decodeTo: UserData.self)
// do something with the response
} catch let error as NetworkingError {
switch error {
case .couldNotBuildURLRequest(reason: let reason):
// url inside of your Request is invalid
case .decodingFailed(reason: let reason):
// could not decode data into your Decodable type
case .responseValidationFailed(reason: let reason):
// bad URLResponse
case .requestFailed(reason: let reason):
// api request failed
}
}5.1.0
What's new?
This patch is mainly an internal refactoring for Swift concurrency.
The biggest difference impacting clients is the return type of the following methods:
RequestPerformable.performTask(...) -> URLSessionDataTaskFileDownloadable.downloadFileTask(...) -> URLSessionDownloadTaskDataUploadable.uploadDataTask(...) -> URLSessionUploadTaskFileUploadable.uploadFileTask(...) -> URLSessionUploadTask
Each of these methods no longer returns subclasses of URLSessionTask. Instead, they now return a new abstract CancellableRequest.
Why was this change made?
Each of the protocols listed above has variants of the same method to support multiple programming paradigms (Swift Concurrency, completion handlers, Combine). To prevent having repeated internal logic, one method was chosen as the "core" implementation, while the rest are adapter methods.
Previously, I was using the completion handler-based method (using URLSession.dataTask() and similar methods) as the root implementation, while all other methods were adapters. I also returned the URLSessionTask in order to allow clients the ability to call methods such as .suspend() or .cancel().
However, Apple has recommended that all new networking code moving forward be written in Swift Concurrency, and to adapt to completion handlers or Publishers for legacy support. Following that recommendation, I refactored every networking class to use async/await as the core implementation and have the other methods be adapters.
Before
RequestPerformer.perform() - adapter
RequestPerformer.performTask() - CORE
RequestPerformer.performPublisher() - adapter
After
RequestPerformer.perform() - CORE
RequestPerformer.performTask() - adapter
RequestPerformer.performPublisher() - adapter
This means that I am no longer internally using URLSession.dataTask() (and similar methods) but rather URLSession.data() (and similar methods), meaning I no longer have access to the URLSessionTask objects.
In order to keep consistent behavior, I created a new CancellableRequest class which mimics URLSessionTask as closely as possible, with .resume() and .cancel() methods. Calling .cancel() is also able to also cancel the underlying Task used, thus also being able to stop the internal Swift Concurrency task.
5.0.4
What's new?
Updated how session management is done
Now instead of injecting a URLSession and a SessionDelegate into different request performing classes, clients now inject Session which internally holds those values.
Before
let urlSession = URLSession(...)
let delegate: SessionDelegate()
RequestPerformer(urlSession: urlSession, delegate: delegate)After
let delegate: SessionDelegate()
let session = Session(delegate: delegate)
RequestPerformer(session: session)Why did I make these changes?
A) easier to manage
B) reduce large init methods since I enforce URLSession.delegate to be of type SessionDelegate in order for different interceptors to work properly.