Skip to content

achdif/NetworkKit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

4 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🌐 NetworkKit

Swift Platform SPM Compatible License Swift Package Manager

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.

✨ Features

  • πŸš€ 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

πŸ“‹ Requirements

Platform Minimum Version
iOS 13.0+
macOS 10.15+
tvOS 13.0+
watchOS 6.0+
Xcode 13.0+
Swift 5.5+

πŸ“¦ Installation

Swift Package Manager (Xcode 12+)

  1. In Xcode, select File > Add Packages...
  2. Enter the repository URL: https://github.com/achdif/NetworkKit.git
  3. Select the version you'd like to use
  4. Click Add Package

Swift Package Manager (Package.swift)

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 main branch. Replace with a version tag once the first release is created.

πŸš€ Getting Started

Basic Usage

  1. Import the framework
import NetworkKit
import Combine
  1. 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
    }()
}
  1. 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))
    }
}
  1. 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)
    }
}

πŸ”Œ Advanced Usage

Interceptors

NetworkKit provides a powerful interceptor system to modify requests and responses.

Built-in Interceptors

// 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
    ]
)

Creating Custom Interceptors

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)
        }
    }
}

Error Handling

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"
        }
    }
}

Request Configuration

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]
)

πŸ§ͺ Testing

NetworkKit is designed with testability in mind. Here's how you can test your networking layer:

Mocking Network Responses

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)
    }
}

MockURLProtocol Implementation

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() {}
}

🌟 Example Project

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!

🀝 Contributing

Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
  3. Commit your Changes (git commit -m 'Add some AmazingFeature')
  4. Push to the Branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

Code Style

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

πŸ“„ License

Distributed under the MIT License. See LICENSE for more information.

πŸ‘¨β€πŸ’» Author

πŸ™Œ Acknowledgments

  • Thanks to all contributors who have helped improve this project
  • Inspired by Moya and Alamofire
  • Built with ❀️ using Swift

About

SPM NetworkKit

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages