EZNetworking is a powerful, lightweight Swift networking library that simplifies API interactions in your iOS applications. Built with modern Swift features, it provides an intuitive interface for making HTTP requests, handling responses, and managing network operations.
- Modern Swift Support: Built with Swift 5.9 and iOS 15.0+
- Async/Await Integration: First-class support for Swift concurrency
- Type-Safe Networking: Strong typing for requests and responses
- Flexible Request Building: Multiple approaches to creating requests
- Comprehensive Interceptors: Full request/response pipeline control
- Built-in Caching: Efficient response caching system
- File & Image Downloads: Easy-to-use download utilities
- Extensive Testing: 100% unit test coverage
- Installation
- Quick Start Guide
- Building Requests
- Request Components
- Making Network Calls
- Download Features
- Advanced Features
- Error Handling
- Contributing
- License
Add EZNetworking to your project using Swift Package Manager:
dependencies: [
.package(url: "https://github.com/Aldo10012/EZNetworking.git", from: "3.1.0")
]
Or through Xcode:
- Go to File > Add Packages
- Enter:
https://github.com/Aldo10012/EZNetworking.git
- Select version: 3.1.0 or later
Here's a simple example to get you started:
// Create a request
let request = RequestFactoryImpl().build(
httpMethod: .GET,
urlString: "https://api.example.com/data",
parameters: [.init(key: "userId", value: "123")]
)
// Using async/await
do {
let response = try await AsyncRequestPerformer().perform(
request: request,
decodeTo: UserData.self
)
print("User data: \(response)")
} catch {
print("Error: \(error)")
}
EZNetworking provides three ways to create requests:
- Using RequestFactory for quick, one-line requests
- Using RequestBuilder for step-by-step request construction
- Implementing the Request protocol for reusable API endpoints
Perfect for quick, one-line request creation:
let request = RequestFactoryImpl().build(
httpMethod: .POST,
urlString: "https://api.example.com/users",
parameters: [
.init(key: "name", value: "John Doe"),
.init(key: "email", value: "john@example.com")
],
headers: [
.accept(.json),
.contentType(.json)
],
body: .jsonString("{\"role\":\"user\"}"),
timeoutInterval: 30,
cachePolicy: .useProtocolCachePolicy
)
Ideal for complex requests with multiple configurations:
let request = RequestBuilderImpl()
.setHttpMethod(.POST)
.setBaseUrl("https://api.example.com")
.setParameters([
.init(key: "api_version", value: "v2")
])
.setHeaders([
.accept(.json),
.authorization(.bearer("YOUR_TOKEN"))
])
.setBody(.jsonString("{\"data\":\"value\"}"))
.setTimeoutInterval(30)
.setCachePolicy(.useProtocolCachePolicy)
.build()
The Request protocol allows you to create reusable request definitions:
struct UserRequest: Request {
let userId: String
var httpMethod: HTTPMethod { .GET }
var baseUrlString: String { "https://api.example.com" }
var parameters: [HTTPParameter]? {[
.init(key: "user_id", value: userId),
.init(key: "version", value: "v2")
]}
var headers: [HTTPHeader]? {[
.accept(.json),
.contentType(.json),
.authorization(.bearer("YOUR_TOKEN"))
]}
var body: HTTPBody? { nil }
var timeoutInterval: TimeInterval { 30 }
var cachePolicy: URLRequest.CachePolicy { .useProtocolCachePolicy }
}
// Usage
let userRequest = UserRequest(userId: "123")
let response = try await AsyncRequestPerformer().perform(
request: userRequest,
decodeTo: UserData.self
)
Supported HTTP methods:
public enum HTTPMethod: String {
case GET, POST, PUT, DELETE
}
Add query parameters to your requests:
let parameters: [HTTPParameter] = [
.init(key: "page", value: "1"),
.init(key: "limit", value: "20"),
.init(key: "sort", value: "desc")
]
// With RequestFactory
let request1 = RequestFactoryImpl().build(
httpMethod: .GET,
urlString: "https://api.example.com",
parameters: parameters
)
// With RequestBuilder
let request2 = RequestBuilderImpl()
.setHttpMethod(.GET)
.setBaseUrl("https://api.example.com")
.setParameters(parameters)
.build()
EZNetworking provides a type-safe way to add headers:
let headers: [HTTPHeader] = [
.accept(.json),
.contentType(.json),
.authorization(.bearer("YOUR_TOKEN")),
.custom(key: "X-Custom-Header", value: "custom-value")
]
// Common header types
public enum HTTPHeader {
case accept(ContentType)
case contentType(ContentType)
case authorization(AuthorizationType)
case custom(key: String, value: String)
// ... other http header types
}
public enum ContentType: String {
case json = "application/json"
case xml = "application/xml"
case formUrlEncoded = "application/x-www-form-urlencoded"
case multipartFormData = "multipart/form-data"
// ... other content types
}
Multiple authorization methods are supported:
// Bearer token
.authorization(.bearer("YOUR_TOKEN"))
// Custom auth
.authorization(.custom("Custom-Auth-Value"))
Multiple body types are supported:
// JSON String
let jsonBody = HTTPBody.jsonString("{\"key\":\"value\"}")
// Data
let dataBody = HTTPBody.data(someData)
// Form URL Encoded
let formBody = HTTPBody.formUrlEncoded([
"key1": "value1",
"key2": "value2"
])
// Multipart Form Data
let multipartBody = HTTPBody.multipartFormData([
.init(name: "file", fileName: "image.jpg", data: imageData),
.init(name: "description", value: "Profile picture")
])
Configure request timeout and caching behavior:
// With RequestFactory
let request1 = RequestFactoryImpl().build(
httpMethod: .GET,
urlString: "https://api.example.com",
timeoutInterval: 30,
cachePolicy: .returnCacheDataElseLoad
)
// With RequestBuilder
let request2 = RequestBuilderImpl()
.setHttpMethod(.GET)
.setBaseUrl("https://api.example.com")
.setTimeoutInterval(30)
.setCachePolicy(.returnCacheDataElseLoad)
.build()
Modern Swift concurrency support:
// With response decoding
do {
let userData = try await AsyncRequestPerformer().perform(request: request, decodeTo: UserData.self)
// Handle decoded response
} catch {
// Handle error
}
// Without decoding
do {
try await AsyncRequestPerformer().perform(request: request)
// Handle success
} catch {
// Handle error
}
Traditional callback-based approach:
// With response decoding
RequestPerformer().performTask(request: request,decodeTo: UserData.self) { result in
switch result {
case .success(let userData):
// Handle decoded response
case .failure(let error):
// Handle error
}
}
// Without decoding
RequestPerformer().performTask(request: request) { result in
switch result {
case .success:
// Handle success
case .failure(let error):
// Handle error
}
}
Control over URLSessionTask:
// Store task reference
let task = RequestPerformer().performTask(request: request) { _ in
// Handle completion
}
// Cancel task if needed
task.cancel()
// Resume suspended task
task.resume()
// Suspend task
task.suspend()
// Get task state
print(task.state) // running, suspended, canceling, completed
let fileURL = URL(string: "https://example.com/file.pdf")!
// Async/await
do {
let localURL = try await FileDownloader().downloadFile(with: fileURL)
// Handle downloaded file
} catch {
// Handle error
}
// Completion handler with progress tracking
let task = FileDownloader().downloadFile(url: testURL) { result in
switch result {
case .success:
// handle the returned local URL path. Perhaps write and save it in FileManager
case .failure(let error):
// handle error
}
}
// Cancel download if needed
task.cancel()
let imageURL = URL(string: "https://example.com/image.jpg")!
// Async/await
do {
let image = try await ImageDownloader().downloadImage(from: imageURL)
// Use downloaded image
} catch {
// Handle error
}
// Completion handler with caching
let task = ImageDownloader().downloadImageTask(url: imageURL) { result in
switch result {
case .success:
// handle success
case .failure(let error):
// handle error
}
}
EZNetworking provides a comprehensive set of interceptors for customizing network behavior:
Control caching behavior:
class CustomCacheInterceptor: CacheInterceptor {
func urlSession(
_ session: URLSession,
dataTask: URLSessionDataTask,
willCacheResponse proposedResponse: CachedURLResponse
) async -> CachedURLResponse? {
// Customize caching behavior
return proposedResponse
}
}
let delegate = SessionDelegate()
delegate.cacheInterceptor = CustomCacheInterceptor()
Handle authentication challenges:
class CustomAuthInterceptor: AuthenticationInterceptor {
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge
) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
// Handle authentication
return (.useCredential, URLCredential(
user: "username",
password: "password",
persistence: .forSession
))
}
}
let delegate = SessionDelegate()
delegate.authenticationInterceptor = CustomAuthInterceptor()
Control URL redirections:
class CustomRedirectInterceptor: RedirectInterceptor {
func urlSession(
_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest
) async -> URLRequest? {
// Handle redirection
return request
}
}
let delegate = SessionDelegate()
delegate.redirectInterceptor = CustomRedirectInterceptor()
Collect performance metrics:
class CustomMetricsInterceptor: MetricsInterceptor {
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didFinishCollecting metrics: URLSessionTaskMetrics
) {
// Process metrics
print("Task duration: \(metrics.taskInterval.duration)")
}
}
let delegate = SessionDelegate()
delegate.metricsInterceptor = CustomMetricsInterceptor()
Monitor task lifecycle events:
class CustomLifecycleInterceptor: TaskLifecycleInterceptor {
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?
) {
// Handle task completion
}
func urlSession(
_ session: URLSession,
taskIsWaitingForConnectivity task: URLSessionTask
) {
// Handle connectivity waiting
}
func urlSession(
_ session: URLSession,
didCreateTask task: URLSessionTask
) {
// Handle task creation
}
}
let delegate = SessionDelegate()
delegate.taskLifecycleInterceptor = CustomLifecycleInterceptor()
Process incoming data:
class CustomDataTaskInterceptor: DataTaskInterceptor {
func urlSession(
_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive data: Data
) {
// Process received data
}
func urlSession(
_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive response: URLResponse
) async -> URLSession.ResponseDisposition {
// Handle response
return .allow
}
}
let delegate = SessionDelegate()
delegate.dataTaskInterceptor = CustomDataTaskInterceptor()
Monitor download progress:
class CustomDownloadInterceptor: DownloadTaskInterceptor {
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL
) {
// Handle download completion
}
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64
) {
// Track download progress
let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
print("Download progress: \(progress)")
}
}
let delegate = SessionDelegate()
delegate.downloadTaskInterceptor = CustomDownloadInterceptor()
Handle streaming operations:
class CustomStreamInterceptor: StreamTaskInterceptor {
func urlSession(
_ session: URLSession,
streamTask: URLSessionStreamTask,
didBecome inputStream: InputStream,
outputStream: OutputStream
) {
// Handle streams
}
func urlSession(
_ session: URLSession,
readClosedFor streamTask: URLSessionStreamTask
) {
// Handle read close
}
}
let delegate = SessionDelegate()
delegate.streamTaskInterceptor = CustomStreamInterceptor()
Handle WebSocket communications:
class CustomWebSocketInterceptor: WebSocketTaskInterceptor {
func urlSession(
_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didOpenWithProtocol protocol: String?
) {
// Handle WebSocket open
}
func urlSession(
_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
reason: Data?
) {
// Handle WebSocket close
}
}
let delegate = SessionDelegate()
delegate.webSocketTaskInterceptor = CustomWebSocketInterceptor()
Configure and manage URLSession behavior:
// Create session delegate with interceptors
let delegate = SessionDelegate()
delegate.cacheInterceptor = CustomCacheInterceptor()
delegate.authenticationInterceptor = CustomAuthInterceptor()
delegate.metricsInterceptor = CustomMetricsInterceptor()
// Create performer with custom session delegate. Works for RequestPerformer and AsyncRequestPerformer
let performer = RequestPerformer(sessionDelegate: delegate)
// Use performer for requests
performer.performTask(request: request) { result in
// Handle result
}
EZNetworking provides comprehensive error handling:
public enum NetworkingError: Error {
// Internal errors
case internalError(InternalError) /// any internal error
// HTTP Status Code errors
case information(HTTPInformationalStatus, URLResponseHeaders) /// 1xx status code errors
case redirect(HTTPRedirectionStatus, URLResponseHeaders) /// 3xx status code errors
case httpClientError(HTTPClientErrorStatus, URLResponseHeaders) /// 4xx status code errors
case httpServerError(HTTPServerErrorStatus, URLResponseHeaders) /// 5xx status code errors
// URL Errors
case urlError(URLError) /// any URL error
}
// Error handling example
do {
let response = try await AsyncRequestPerformer().perform(request: request, decodeTo: UserData.self)
// do something with response
} catch let error as NetworkingError {
switch error {
case .internalError(let internalError):
// some internal error such as failed to decode or url not valid
case .information(let hTTPInformationalStatus, let uRLResponseHeaders):
// .. some 1xx status code error
case .redirect(let hTTPRedirectionStatus, let uRLResponseHeaders):
// some 3xx status code error
case .httpClientError(let hTTPClientErrorStatus, let uRLResponseHeaders):
// some 4xx status code error
case .httpServerError(let hTTPServerErrorStatus, let uRLResponseHeaders):
// some 5xx status code error
case .urlError(let uRLError):
// some error of type URLError
}
}
Contributions are welcome! If you have an idea to improve EZNetworking, please feel free to submit and open a pull request or open an issue.
EZNetworking is available under the MIT license. See the LICENSE file for more info.