NetworkLayer is a modern, type-safe Swift framework for elegant network communication. Built with Swift's async/await concurrency model and actor-based architecture, it provides a robust foundation for making HTTP requests with features like authentication handling, retry policies, and request processing.
β¨ Type-Safe Requests - Protocol-based request modeling with compile-time safety
β‘ Async/Await Native - Built for modern Swift concurrency with actor-based thread safety
π Authentication Support - Built-in authentication interceptor with credential refresh
π Retry Policies - Powered by Typhoon for robust failure handling
π― Flexible Configuration - Customizable session configuration, decoders, and delegates
π± Cross-Platform - Works on iOS, macOS, tvOS, watchOS, and visionOS
β‘ Lightweight - Minimal footprint with focused dependencies
π§ͺ Well Tested - Comprehensive test coverage
- Requirements
- Installation
- Architecture
- Quick Start
- Usage
- Common Use Cases
- Communication
- Documentation
- Contributing
- Author
- Dependencies
- License
| Platform | Minimum Version |
|---|---|
| iOS | 13.0+ |
| macOS | 10.15+ |
| tvOS | 13.0+ |
| watchOS | 7.0+ |
| visionOS | 1.0+ |
| Xcode | 15.3+ |
| Swift | 5.10+ |
Add the following dependency to your Package.swift:
dependencies: [
.package(url: "https://github.com/space-code/network-layer.git", from: "1.1.0")
]Or add it through Xcode:
- File > Add Package Dependencies
- Enter package URL:
https://github.com/space-code/network-layer.git - Select version requirements
NetworkLayer consists of two packages:
- NetworkLayer - Core functionality including request processing, session management, and response handling
- NetworkLayerInterfaces - Protocol definitions and interfaces for extensibility and testing
This separation allows for better modularity and makes it easy to mock components during testing.
import NetworkLayer
import NetworkLayerInterfaces
// Define your request
struct UserRequest: IRequest {
let userId: String
var domainName: String { "https://api.example.com" }
var path: String { "users/\(userId)" }
var httpMethod: HTTPMethod { .get }
}
// Make the request
let requestProcessor = NetworkLayerAssembly().assemble()
do {
let response: Response<User> = try await requestProcessor.send(UserRequest(userId: "123"))
print("β
User fetched: \(response.value.name)")
} catch {
print("β Request failed: \(error)")
}Define requests by conforming to the IRequest protocol:
import NetworkLayerInterfaces
struct GetPostsRequest: IRequest {
var domainName: String { "https://jsonplaceholder.typicode.com" }
var path: String { "posts" }
var httpMethod: HTTPMethod { .get }
}
struct CreatePostRequest: IRequest {
let title: String
let body: String
let userId: Int
var domainName: String { "https://jsonplaceholder.typicode.com" }
var path: String { "posts" }
var httpMethod: HTTPMethod { .post }
var body: RequestBody? {
.dictionary([
"title": title,
"body": body,
"userId": userId
])
}
}
// Usage
let requestProcessor = NetworkLayerAssembly().assemble()
// GET request
let posts: Response<[Post]> = try await requestProcessor.send(GetPostsRequest())
// POST request
let newPost: Response<Post> = try await requestProcessor.send(
CreatePostRequest(title: "Hello", body: "World", userId: 1)
)NetworkLayer supports authentication through the IAuthenticationInterceptor protocol:
import NetworkLayerInterfaces
class BearerTokenInterceptor: IAuthenticationInterceptor {
private var token: String?
func adapt(request: inout URLRequest, for session: URLSession) async throws {
if let token = token {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
}
func isRequireRefresh(_ request: URLRequest, response: HTTPURLResponse) -> Bool {
response.statusCode == 401
}
func refresh(_ request: URLRequest, with response: HTTPURLResponse, for session: URLSession) async throws {
// Implement token refresh logic
token = try await refreshToken()
}
}
// Configure with authentication
let configuration = Configuration(
sessionConfiguration: .default,
interceptor: BearerTokenInterceptor()
)
let requestProcessor = NetworkLayerAssembly().assemble(configuration: configuration)
// Requests requiring authentication
struct SecureRequest: IRequest {
var domainName: String { "https://api.example.com" }
var path: String { "secure/data" }
var httpMethod: HTTPMethod { .get }
var requiresAuthentication: Bool { true }
}Leverage Typhoon for sophisticated retry strategies:
import Typhoon
// Configure retry policy during assembly
let requestProcessor = NetworkLayerAssembly(
retryStrategy: .custom(
.exponentialWithJitter(
retry: 3,
jitterFactor: 0.2,
maxInterval: .seconds(30),
multiplier: 2.0,
duration: .seconds(1)
)
)
).assemble()
// Per-request retry strategy override
let response: Response<Data> = try await requestProcessor.send(
request,
strategy: .constant(retry: 5, duration: .seconds(2))
)
// Custom retry evaluation
let response: Response<User> = try await requestProcessor.send(
request,
shouldRetry: { error in
// Only retry on network errors, not on validation failures
if let networkError = error as? URLError {
return networkError.code == .timedOut || networkError.code == .networkConnectionLost
}
return false
}
)Customize the network layer to fit your needs:
import NetworkLayer
import NetworkLayerInterfaces
class CustomDelegate: RequestProcessorDelegate {
func requestProcessor(
_ processor: IRequestProcessor,
willSendRequest request: URLRequest
) async throws {
print("Sending request to: \(request.url?.absoluteString ?? "")")
}
func requestProcessor(
_ processor: IRequestProcessor,
validateResponse response: HTTPURLResponse,
data: Data,
task: URLSessionTask
) throws {
guard (200...299).contains(response.statusCode) else {
throw NetworkLayerError.invalidStatusCode(response.statusCode)
}
}
}
let configuration = Configuration(
sessionConfiguration: .default,
sessionDelegate: CustomSessionDelegate(),
sessionDelegateQueue: .main,
jsonDecoder: JSONDecoder(),
interceptor: BearerTokenInterceptor()
)
let requestProcessor = NetworkLayerAssembly().assemble(
configuration: configuration,
delegate: CustomDelegate()
)Add custom validation logic for responses:
class ValidationDelegate: RequestProcessorDelegate {
func requestProcessor(
_ processor: IRequestProcessor,
validateResponse response: HTTPURLResponse,
data: Data,
task: URLSessionTask
) throws {
// Check status code
guard (200...299).contains(response.statusCode) else {
throw APIError.invalidStatusCode(response.statusCode)
}
// Check content type
guard let contentType = response.value(forHTTPHeaderField: "Content-Type"),
contentType.contains("application/json") else {
throw APIError.invalidContentType
}
// Check response size
guard data.count > 0 else {
throw APIError.emptyResponse
}
}
}import NetworkLayer
import NetworkLayerInterfaces
class APIClient {
private let requestProcessor: IRequestProcessor
init() {
requestProcessor = NetworkLayerAssembly(
retryStrategy: .custom(
.exponentialWithJitter(
retry: 3,
jitterFactor: 0.2,
maxInterval: .seconds(30),
multiplier: 2.0,
duration: .seconds(1)
)
)
).assemble()
}
func fetchUser(id: String) async throws -> User {
struct UserRequest: IRequest {
let id: String
var domainName: String { "https://api.example.com" }
var path: String { "users/\(id)" }
var httpMethod: HTTPMethod { .get }
}
let response: Response<User> = try await requestProcessor.send(
UserRequest(id: id)
)
return response.value
}
func updateUser(_ user: User) async throws -> User {
struct UpdateUserRequest: IRequest {
let user: User
var domainName: String { "https://api.example.com" }
var path: String { "users/\(user.id)" }
var httpMethod: HTTPMethod { .put }
var body: RequestBody? {
.encodable(user)
}
}
let response: Response<User> = try await requestProcessor.send(
UpdateUserRequest(user: user)
)
return response.value
}
}import NetworkLayer
import NetworkLayerInterfaces
class SecureAPIClient {
private let requestProcessor: IRequestProcessor
init(authToken: String) {
class AuthInterceptor: IAuthenticationInterceptor {
var token: String
init(token: String) {
self.token = token
}
func adapt(request: inout URLRequest, for session: URLSession) async throws {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
func isRequireRefresh(_ request: URLRequest, response: HTTPURLResponse) -> Bool {
response.statusCode == 401
}
func refresh(_ request: URLRequest, with response: HTTPURLResponse, for session: URLSession) async throws {
// Refresh token logic
}
}
let configuration = Configuration(
sessionConfiguration: .default,
interceptor: AuthInterceptor(token: authToken)
)
requestProcessor = NetworkLayerAssembly().assemble(configuration: configuration)
}
func fetchPrivateData() async throws -> PrivateData {
struct PrivateDataRequest: IRequest {
var domainName: String { "https://api.example.com" }
var path: String { "private/data" }
var httpMethod: HTTPMethod { .get }
var requiresAuthentication: Bool { true }
}
let response: Response<PrivateData> = try await requestProcessor.send(
PrivateDataRequest()
)
return response.value
}
}- π Found a bug? Open an issue
- π‘ Have a feature request? Open an issue
- β Questions? Start a discussion
- π Security issue? Email [email protected]
Comprehensive documentation is available: NetworkLayer Documentation
We love contributions! Please feel free to help out with this project. If you see something that could be made better or want a new feature, open up an issue or send a Pull Request.
Bootstrap the development environment:
mise installNikita Vasilev
- Email: [email protected]
- GitHub: @ns-vasilev
This project uses several open-source packages:
- Atomic - A Swift property wrapper designed to make values thread-safe
- Typhoon - A service for retry policies with multiple strategies
- Mocker - A library for mocking data requests using a custom URLProtocol
network-layer is available under the MIT license. See the LICENSE file for more info.
Made with β€οΈ by space-code
