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
- π± Cross-Platform - Supports all Apple platforms (iOS, macOS, tvOS, watchOS)
- π Authentication - Easy integration with authentication flows
- π¦ Modular Design - Highly customizable and extensible architecture
- π Built-in Logging - Automatic request/response logging with emoji indicators
- π Snake Case Decoding - Automatic conversion from snake_case to camelCase
| 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 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]
}
}
var interceptors: [RequestInterceptorProtocol] {
switch self {
case .getUser:
return [DefaultHeadersInterceptor()]
case .updateUser:
return [
DefaultHeadersInterceptor(),
AuthInterceptor(tokenProvider: { UserDefaults.standard.string(forKey: "authToken") })
]
}
}
}
// 2. Create the network service with base URL
private let networkService = NetworkService<UserEndpoint>(baseURL: "https://api.example.com")
// 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.
NetworkKit provides several built-in interceptors for common use cases:
// 1. API Key Interceptor - Adds API key to headers
let apiKeyInterceptor = APIKeyInterceptor(apiKey: "your-api-key", headerField: "X-API-Key")
// 2. Default Headers Interceptor - Adds default headers like Content-Type and Accept
let headersInterceptor = DefaultHeadersInterceptor(headers: [
"Accept-Language": Locale.current.languageCode ?? "en"
])
// 3. Authentication Interceptor - Adds Bearer token to Authorization header
let authInterceptor = AuthInterceptor(tokenProvider: {
return "your-bearer-token"
})
// 4. Query Params Interceptor - Adds default query parameters (e.g., language)
let queryParamsInterceptor = QueryParamsInterceptor()Create custom interceptors by implementing RequestInterceptorProtocol:
class CustomInterceptor: RequestInterceptorProtocol {
private let sessionId: String
init(sessionId: String) {
self.sessionId = sessionId
}
func intercept(_ urlRequest: URLRequest) -> URLRequest {
var request = urlRequest
request.addValue(sessionId, forHTTPHeaderField: "X-Session-ID")
return request
}
}NetworkKit provides comprehensive error handling through the NetworkError enum:
public enum NetworkError: LocalizedError {
case invalidURL
case invalidResponse
case decodingError(Error)
case networkError(Error)
case serverError(statusCode: Int)
case unauthorized
case forbidden
case notFound
case unknown
public var errorDescription: String? {
switch self {
case .invalidURL:
return "Invalid URL"
case .invalidResponse:
return "Invalid response from server"
case .decodingError(let error):
return "Failed to decode response: \(error.localizedDescription)"
case .networkError(let error):
return "Network error: \(error.localizedDescription)"
case .serverError(let statusCode):
return "Server error with status code: \(statusCode)"
case .unauthorized:
return "Unauthorized access"
case .forbidden:
return "Access forbidden"
case .notFound:
return "Resource not found"
case .unknown:
return "Unknown error occurred"
}
}
public var title: String {
switch self {
case .invalidURL, .invalidResponse, .decodingError, .unknown:
return "Data Error"
case .networkError:
return "Connection Error"
case .serverError:
return "Server Error"
case .unauthorized:
return "Unauthorized"
case .forbidden:
return "Forbidden"
case .notFound:
return "Not Found"
}
}
public var imageName: String {
switch self {
case .invalidURL, .invalidResponse, .decodingError, .unknown:
return "error_icon"
case .networkError:
return "network_error_icon"
case .serverError:
return "server_error_icon"
case .unauthorized:
return "unauthorized_icon"
case .forbidden:
return "forbidden_icon"
case .notFound:
return "not_found_icon"
}
}
}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