A modern, lightweight, and protocol-oriented networking layer for Swift applications, built on top of URLSession and Combine. NetworkKit simplifies API requests, error handling, and request/response interception while providing a clean and maintainable API for all your networking needs.
- π Simple & Intuitive API - Clean and easy-to-use API for making network requests
- π Combine-Powered - Built with Combine for reactive programming and async/await support
- π Interceptor Support - Modify requests and responses with custom interceptors
- π‘οΈ Comprehensive Error Handling - Detailed error types and easy error handling
- β‘ Performance Optimized - Lightweight and efficient networking layer
- π§ͺ Fully Tested - Comprehensive test coverage for reliability
- π± Cross-Platform - Supports all Apple platforms (iOS, macOS, tvOS, watchOS)
- π Request Retry - Built-in support for request retry logic
- π Authentication - Easy integration with authentication flows
- π¦ Modular Design - Highly customizable and extensible architecture
| Platform | Minimum Version |
|---|---|
| iOS | 13.0+ |
| macOS | 10.15+ |
| tvOS | 13.0+ |
| watchOS | 6.0+ |
| Xcode | 13.0+ |
| Swift | 5.5+ |
- In Xcode, select File > Add Packages...
- Enter the repository URL:
https://github.com/achdif/NetworkKit.git - Select the version you'd like to use
- Click Add Package
Add the following to your Package.swift file:
dependencies: [
.package(url: "https://github.com/achdif/NetworkKit.git", branch: "main")
]Then add NetworkKit to your target's dependencies:
targets: [
.target(
name: "YourTarget",
dependencies: ["NetworkKit"]
)
]Note: Currently using the
mainbranch. Replace with a version tag once the first release is created.
- Import the framework
import NetworkKit
import Combine- Define your model
struct User: Codable, Identifiable {
let id: Int
let name: String
let email: String
let createdAt: Date
}
// If you need custom date decoding
enum DateFormatters {
static let iso8601Full: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
formatter.calendar = Calendar(identifier: .iso8601)
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}()
}- Make a request
class UserService {
private let networkService: NetworkServiceProtocol = NetworkService()
private var cancellables = Set<AnyCancellable>()
// 1. Define your request type
enum UserEndpoint: NetworkRequest {
case getUser(id: Int)
case updateUser(user: User)
var endpoint: String {
switch self {
case .getUser(let id):
return "/users/\(id)"
case .updateUser(let user):
return "/users/\(user.id)"
}
}
var method: HTTPMethod {
switch self {
case .getUser:
return .GET
case .updateUser:
return .PUT
}
}
var parameters: [String: Any]? {
switch self {
case .getUser:
return nil
case .updateUser(let user):
// Convert user to dictionary or use JSONEncoder
return ["name": user.name, "email": user.email]
}
}
}
// 2. Create the network service
private let networkService = NetworkService<UserEndpoint>()
// 3. Make requests
func fetchUser(userId: Int) -> AnyPublisher<User, NetworkError> {
return networkService.request(.getUser(id: userId))
}
func updateUser(_ user: User) -> AnyPublisher<User, NetworkError> {
return networkService.request(.updateUser(user: user))
}
}- Use in your view model
class UserViewModel: ObservableObject {
@Published var user: User?
@Published var error: NetworkError?
@Published var isLoading = false
private let userService = UserService()
private var cancellables = Set<AnyCancellable>()
func loadUser(userId: Int) {
isLoading = true
userService.fetchUser(userId: userId)
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.error = error
}
} receiveValue: { [weak self] user in
self?.user = user
}
.store(in: &cancellables)
}
}NetworkKit provides a powerful interceptor system to modify requests and responses.
// 1. API Key Interceptor
let apiKeyInterceptor = APIKeyInterceptor(apiKey: "your-api-key", headerField: "X-API-Key")
// 2. Headers Interceptor
let headersInterceptor = DefaultHeadersInterceptor(headers: [
"Content-Type": "application/json",
"Accept": "application/vnd.api+json",
"Accept-Language": Locale.current.languageCode ?? "en"
])
// 3. Authentication Interceptor
let authInterceptor = AuthInterceptor(tokenProvider: {
return "your-bearer-token"
})
// 4. Logging Interceptor
let loggingInterceptor = LoggingInterceptor(logLevel: .debug)
// Usage
networkService.request(
endpoint: "/users/me",
method: .get,
interceptors: [
headersInterceptor,
authInterceptor,
loggingInterceptor
]
)Create custom interceptors by implementing RequestInterceptorProtocol:
class CustomInterceptor: RequestInterceptorProtocol {
private let sessionId: String
init(sessionId: String) {
self.sessionId = sessionId
}
func adapt(_ urlRequest: URLRequest) -> URLRequest {
var request = urlRequest
request.addValue(sessionId, forHTTPHeaderField: "X-Session-ID")
return request
}
func retry(_ request: URLRequest, for session: URLSession, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
// Implement custom retry logic here
if (error as NSError).code == NSURLErrorTimedOut {
completion(.retryWithDelay(2.0)) // Retry after 2 seconds
} else {
completion(.doNotRetry)
}
}
}NetworkKit provides comprehensive error handling through the NetworkError enum:
public enum NetworkError: Error, LocalizedError {
case invalidURL
case invalidResponse
case statusCode(Int)
case decoding(Error)
case url(URLError)
case unknown(Error)
case noInternetConnection
case timeout
case unauthorized
case forbidden
case notFound
case serverError
public var errorDescription: String? {
switch self {
case .invalidURL:
return "Invalid URL"
case .invalidResponse:
return "Invalid response from server"
case .statusCode(let code):
return "HTTP Error: \(code)"
case .decoding(let error):
return "Failed to decode response: \(error.localizedDescription)"
case .url(let error):
return error.localizedDescription
case .unknown(let error):
return "Unknown error: \(error.localizedDescription)"
case .noInternetConnection:
return "No internet connection"
case .timeout:
return "Request timed out"
case .unauthorized:
return "Unauthorized access"
case .forbidden:
return "Access forbidden"
case .notFound:
return "Resource not found"
case .serverError:
return "Internal server error"
}
}
}Customize your requests with various configuration options:
// Example with all available options
networkService.request<User>(
endpoint: "/users",
method: .post,
parameters: ["name": "John Doe", "email": "john@example.com"],
headers: ["X-Custom-Header": "value"],
queryParams: ["page": "1", "limit": "20"],
encoding: .json,
timeoutInterval: 30.0,
cachePolicy: .reloadIgnoringLocalCacheData,
interceptors: [authInterceptor, loggingInterceptor]
)NetworkKit is designed with testability in mind. Here's how you can test your networking layer:
import XCTest
@testable import NetworkKit
class NetworkServiceTests: XCTestCase {
var networkService: NetworkService!
var urlSession: URLSession!
var mockURLSession: MockURLSession!
override func setUp() {
super.setUp()
let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [MockURLProtocol.self]
urlSession = URLSession(configuration: config)
networkService = NetworkService(session: urlSession)
}
func testFetchUserSuccess() throws {
// Given
let expectedUser = User(id: 1, name: "Test User", email: "test@example.com", createdAt: Date())
let data = try JSONEncoder().encode(expectedUser)
MockURLProtocol.requestHandler = { request in
let response = HTTPURLResponse(
url: request.url!,
statusCode: 200,
httpVersion: nil,
headerFields: ["Content-Type": "application/json"]
)!
return (response, data)
}
// When
let expectation = self.expectation(description: "Fetch user")
var receivedUser: User?
networkService.request(endpoint: "/users/1", method: .get)
.sink(receiveCompletion: { _ in },
receiveValue: { user in
receivedUser = user
expectation.fulfill()
})
.store(in: &cancellables)
// Then
waitForExpectations(timeout: 1)
XCTAssertEqual(receivedUser?.id, expectedUser.id)
XCTAssertEqual(receivedUser?.name, expectedUser.name)
}
}class MockURLProtocol: URLProtocol {
static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
override class func canInit(with request: URLRequest) -> Bool {
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
guard let handler = MockURLProtocol.requestHandler else {
fatalError("Handler is unavailable.")
}
do {
let (response, data) = try handler(request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} catch {
client?.urlProtocol(self, didFailWithError: error)
}
}
override func stopLoading() {}
}For a complete example of how to use NetworkKit in a real-world application, check out the Example directory in the repository. The example demonstrates:
- Basic and advanced API requests
- Error handling
- Interceptor usage
- Unit testing
- And more!
Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature) - Commit your Changes (
git commit -m 'Add some AmazingFeature') - Push to the Branch (
git push origin feature/AmazingFeature) - Open a Pull Request
Please ensure your code follows the project's style guidelines:
- Use 4 spaces for indentation
- Follow Swift API Design Guidelines
- Document all public interfaces
- Write unit tests for new features
Distributed under the MIT License. See LICENSE for more information.
- Thanks to all contributors who have helped improve this project
- Inspired by Moya and Alamofire
- Built with β€οΈ using Swift