A modern Swift networking library with zero boilerplate. Built with async/await, generic services, and Hilt-style dependency injection. https://youtube.com/shorts/VkT4XkWIoAs?si=_gnR1KXTNG1qT1Lk
- 🚀 Zero Boilerplate - Generic service, ViewModel & views
- 💉 Hilt-Style DI -
@Inject,@HiltViewModelwith caching - 📱 SwiftUI Ready -
NetworkDataViewhandles loading/error/content - 🔐 Auto Token Refresh - Seamless 401 handling
dependencies: [
.package(url: "https://github.com/devzahirul/AsyncSwiftyNetworking.git", from: "1.0.0")
]YourApp/
├── Networking/
│ ├── DI.swift # DI configuration
│ ├── ServiceType.swift # Service typealiases
│ └── ViewModelType.swift # ViewModel typealiases
├── Models/
│ └── Models.swift # API models
└── Views/
└── ...
struct Profile: Codable, HTTPResponseDecodable {
var statusCode: Int?
let id: String
let name: String
let email: String
}
struct User: Codable, HTTPResponseDecodable, Identifiable {
var statusCode: Int?
let id: String
let name: String
}import AsyncSwiftyNetworking
// Fetch services
typealias ProfileService = GenericNetworkService<Profile>
typealias UserListService = GenericListService<User>
// Mutation services (POST/PUT/DELETE)
typealias LoginService = GenericMutationService<LoginRequest, LoginResponse>
typealias SignupService = GenericMutationService<SignupRequest, SignupResponse>import AsyncSwiftyNetworking
// Fetch ViewModels
typealias ProfileViewModel = GenericNetworkViewModel<Profile>
typealias UserListViewModel = GenericListViewModel<User>
// Mutation ViewModels (subclass for custom behavior)
typealias LoginViewModelBase = GenericMutationViewModel<LoginRequest, LoginResponse>import AsyncSwiftyNetworking
enum AppDI {
static func configure() {
DI.configure { di in
di.baseURL = "https://api.example.com"
// Token Storage
let storage = KeychainTokenStorage()
di.registerSingleton(TokenStorage.self, instance: storage)
// Network Client
di.register(URLSessionNetworkClient.self) {
URLSessionNetworkClient.quick(baseURL: di.baseURL)
}
// Services
di.register(ProfileService.self) {
ProfileService(.get("/profile"))
}
di.register(UserListService.self) {
UserListService(.get("/users"))
}
}
}
}@main
struct MyApp: App {
init() {
AppDI.configure()
}
var body: some Scene {
WindowGroup { ContentView() }
}
}struct ProfileView: View {
@HiltViewModel(ProfileViewModel.self) var vm
var body: some View {
NetworkDataView(vm) { profile in
VStack {
Text(profile.name).font(.title)
Text(profile.email).foregroundColor(.secondary)
}
}
}
}
struct UsersView: View {
@HiltViewModel(UserListViewModel.self) var vm
var body: some View {
NetworkListDataView(vm) { user in
Text(user.name)
}
}
}struct LoginRequest: Encodable {
let email: String
let password: String
}
struct LoginResponse: Codable, HTTPResponseDecodable {
var statusCode: Int?
let token: String
let user: User
}typealias LoginService = GenericMutationService<LoginRequest, LoginResponse>di.register(LoginService.self) { LoginService(.post, "/auth/login") }@MainActor
class LoginViewModel: AuthLoginViewModel<LoginRequest, LoginResponse> {
@Published var email = ""
@Published var password = ""
// Auto-extracts token and notifies AuthState!
override func extractToken(from response: LoginResponse) -> String? {
return response.token
}
override func extractUser(from response: LoginResponse) -> (any Sendable)? {
return response.user
}
func login() async {
await execute(LoginRequest(email: email, password: password))
}
}struct ContentView: View {
@ObservedObject var authState = AuthState.shared
var body: some View {
if authState.isLoggedIn {
HomeView()
} else {
LoginView()
}
}
}struct LoginView: View {
@StateObject var vm = LoginViewModel()
var body: some View {
VStack(spacing: 20) {
TextField("Email", text: $vm.email)
.textFieldStyle(.roundedBorder)
SecureField("Password", text: $vm.password)
.textFieldStyle(.roundedBorder)
if let error = vm.error {
Text(error.userMessage).foregroundColor(.red)
}
Button("Login") { Task { await vm.login() } }
.buttonStyle(.borderedProminent)
.disabled(vm.isLoading)
}
.padding()
}
}// Singleton - subscribe to auth state
@ObservedObject var authState = AuthState.shared
// Logout
AuthState.shared.logout()
// Check auth events (Combine)
AuthState.shared.authEvents
.sink { event in
switch event {
case .loggedIn: print("Logged in")
case .loggedOut: print("Logged out")
case .sessionExpired: print("Session expired")
}
}LoginViewModel → AuthState.onLoginSuccess() → Views update
↓
@Published isLoggedIn = true
↓
ContentView.body re-renders → HomeView()
When you register the client with withAuth(), all services automatically get the token header:
DI.configure { di in
let storage = KeychainTokenStorage()
di.registerSingleton(TokenStorage.self, instance: storage)
// All requests auto-include: Authorization: Bearer <token>
di.register(URLSessionNetworkClient.self) {
URLSessionNetworkClient.withAuth(
baseURL: di.baseURL,
tokenStorage: storage,
refreshHandler: MyRefreshHandler()
)
}
// These services use the auth-enabled client
di.register(ProfileService.self) { ProfileService(.get("/profile")) }
di.register(OrderListService.self) { OrderListService(.get("/orders")) }
}GenericNetworkService<Profile> → Resolves URLSessionNetworkClient from DI
↓
AuthInterceptor adds header:
"Authorization: Bearer <token>"
↓
Request sent with token!
Automatically adds Bearer token from TokenStorage:
// Request:
GET /profile
Authorization: Bearer eyJhbGciOi...Handles 401 responses:
class MyRefreshHandler: TokenRefreshHandler {
func refreshToken() async throws -> String {
let response = try await refreshAPI()
return response.accessToken
}
func onRefreshFailure(_ error: Error) async {
AuthState.shared.onSessionExpired() // Navigate to login
}
}// 1. Create custom interceptor
class ApiKeyInterceptor: RequestInterceptor {
private let apiKey: String
init(apiKey: String) {
self.apiKey = apiKey
}
func intercept(_ request: URLRequest) async throws -> URLRequest {
var req = request
req.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
return req
}
}DI.configure { di in
let storage = KeychainTokenStorage()
// Create interceptors
let authInterceptor = AuthInterceptor(storage: storage)
let apiKeyInterceptor = ApiKeyInterceptor(apiKey: "your-api-key")
let loggingInterceptor = LoggingInterceptor(level: .verbose)
// Register client with custom interceptors
di.register(URLSessionNetworkClient.self) {
URLSessionNetworkClient(
configuration: .default,
requestInterceptors: [authInterceptor, apiKeyInterceptor, loggingInterceptor],
responseInterceptors: [loggingInterceptor]
)
}
}Interceptors execute in order:
Request: authInterceptor → apiKeyInterceptor → loggingInterceptor → Network
Response: Network → loggingInterceptor → App
Add default headers to all requests:
class DefaultHeadersInterceptor: RequestInterceptor {
private let headers: [String: String]
init(headers: [String: String]) {
self.headers = headers
}
func intercept(_ request: URLRequest) async throws -> URLRequest {
var req = request
for (key, value) in headers {
req.setValue(value, forHTTPHeaderField: key)
}
return req
}
}
// Usage in DI
di.register(URLSessionNetworkClient.self) {
let defaultHeaders = DefaultHeadersInterceptor(headers: [
"X-API-Key": "your-api-key",
"Accept-Language": "en",
"User-Agent": "MyApp/1.0"
])
let authInterceptor = AuthInterceptor(storage: storage)
return URLSessionNetworkClient(
configuration: .default,
requestInterceptors: [defaultHeaders, authInterceptor],
responseInterceptors: []
)
}// GET
let user: User = try await client.request(.get("/users/1"))
// POST
let created: User = try await client.request(
.post("/users").body(CreateUserRequest(name: "John"))
)
// With query & headers
let results: SearchResponse = try await client.request(
.get("/search")
.query("q", "swift")
.header("X-API-Key", apiKey)
)MIT License