A Network Layer with Alamofire
Over the last five months or so, I have been slowly breaking into native iOS development, and I want to share a few snippets to help other JavaScript developers who are just getting started with Swift and SwiftUI.
A typical pattern in any stack is to create a network layer or HTTP client for communicating with a backend API. Adding a JWT to an HTTP request’s headers is a common requirement when you must authenticate users before performing tasks like creating a new record in the database or retrieving sensitive account information. Here is one example of a quick network layer/manager with Alamofire.
TL;DR
// Manager/NetworkManager.swift
import Alamofire
import Foundation
typealias NetworkError = AFError
enum RequestStatus {
case loading, success, failure
}
class NetworkManager {
static let instance: NetworkManager = .init()
private let baseUrl = "http://localhost:3000" // value from env var or config
private let userDefaults = UserDefaults.standard
func get<Output: Decodable>(_ url: String,
output _: Output.Type,
completion: @escaping (Result<Output, NetworkError>) -> Void)
{
AF.request(baseUrl + url, interceptor: self).validate().responseDecodable(of: Output.self) { response in
completion(response.result)
}
}
func delete<Output: Decodable>(_ url: String,
output _: Output.Type,
completion: @escaping (Result<Output, NetworkError>) -> Void)
{
AF.request(baseUrl + url, method: .delete,
interceptor: self).validate().responseDecodable(of: Output.self) { response in
completion(response.result)
}
}
func post<Input: Encodable, Output: Decodable>(_ url: String,
input: Input,
output _: Output.Type,
completion: @escaping (Result<Output, NetworkError>) -> Void)
{
AF.request(baseUrl + url,
method: .post,
parameters: input,
encoder: .json,
headers: [
.init(name: "Content-Type", value: "application/json"),
],
interceptor: self)
.validate()
.responseDecodable(of: Output.self) { response in
completion(response.result)
}
}
func put<Input: Encodable, Output: Decodable>(_ url: String,
input: Input,
output _: Output.Type,
completion: @escaping (Result<Output, NetworkError>) -> Void)
{
AF.request(baseUrl + url,
method: .put,
parameters: input,
encoder: .json,
headers: [
.init(name: "Content-Type", value: "application/json"),
],
interceptor: self)
.validate()
.responseDecodable(of: Output.self) { response in
completion(response.result)
}
}
func patch<Input: Encodable, Output: Decodable>(_ url: String,
input: Input,
output _: Output.Type,
completion: @escaping (Result<Output, NetworkError>) -> Void)
{
AF.request(baseUrl + url,
method: .patch,
parameters: input,
encoder: .json,
headers: [
.init(name: "Content-Type", value: "application/json"),
],
interceptor: self)
.validate()
.responseDecodable(of: Output.self) { response in
completion(response.result)
}
}
}
extension NetworkManager: RequestInterceptor {
func adapt(_ urlRequest: URLRequest, for _: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var request = urlRequest
if let token = userDefaults.string(forKey: "SUPER_SECURE_JWT") {
let bearerToken = "Bearer \(token)"
request.setValue(bearerToken, forHTTPHeaderField: "Authorization")
}
completion(.success(request))
}
}Usage
import SwiftUI
struct MyView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
NavigationStack {
List {
switch viewModel.requestStatus {
case .loading:
ProgressView().listRowBackground(Color.clear)
case .failure:
Text("Something went wrong")
case .success:
ForEach(viewModel.data, id: \.self) { item in
Text(item)
}
}
}.onAppear {
viewModel.loadItems()
}.navigationTitle("My Items")
}
}
}
extension MyView {
@MainActor class ViewModel: ObservableObject {
private let http = NetworkManager.instance
@Published var requestStatus: RequestStatus = .success
@Published var data: [String] = []
func loadItems() {
requestStatus = .loading
http.get("/items", output: [String].self) { result in
do {
self.data = try result.get()
self.requestStatus = .success
} catch {
self.requestStatus = .failure
}
}
}
}
}TypeScript
In the browser, we can use JavaScript (TS) and the built-in fetch API (coming to Node.js soon) to create an HTTP module and attach a JWT from localStorage to outgoing requests like this:
type HttpMethod = 'GET' | 'PATCH' | 'POST' | 'PUT' | 'DELETE'
const baseUrl = process.env.BASE_API_URL ?? 'http://localhost:3000'
const getConfig = () => {
const base = {
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
}
const token = localStorage.getItem('API_TOKEN')
if (token) {
base.headers['Authorization'] = `Bearer ${token}`
}
return base
}
const request = (method: HttpMethod, url: string, options?: RequestInit) =>
fetch(baseUrl + url, {
method,
...getConfig(),
...options,
} as RequestInit)
const http = {
get: (url: string, options?: RequestInit) => request('GET', url, options),
patch: (url: string, options?: RequestInit) => request('PATCH', url, options),
post: (url: string, options?: RequestInit) => request('POST', url, options),
put: (url: string, options?: RequestInit) => request('PUT', url, options),
delete: (url: string, options?: RequestInit) =>
request('DELETE', url, options),
}
export default httpSwift
In Swift, we can do the same as the above TypeScript example using generics to create a friendly HTTP client for interacting with our APIs.
In this example, I’m using the Alamofire package for submitting HTTP requests rather than the vanilla URLSession class.
I have found Alamofire to be much more enjoyable to work with. We’ll create a NetworkManager class, the only place we import Alamofire. By doing this, we will make it easier on ourselves in the future if we ever need to swap out Alamofire for a different HTTP client/package like RealHTTP.
import Alamofire
import Foundation
class NetworkManager {}We’ll create a typealias for AFError called NetworkError, again, so we only have to import Alamofire once. We’re going to make NetworkManager a singleton and add a property called baseUrl, which is the address of our backend server. The value of baseUrl should be set using an environment variable or config file.
import Alamofire
import Foundation
typealias NetworkError = AFError
class NetworkManager {
static let instance = NetworkManager()
private let baseUrl = "http://localhost:3000"
}Next, we’ll add a get method for submitting GET requests that has an Output parameter with a Decodable constraint to transform JSON to Swift objects, and it will accept a callback function that passes the final result.
import Alamofire
import Foundation
typealias NetworkError = AFError
class NetworkManager {
static let instance = NetworkManager()
private let baseUrl = "http://localhost:3000"
func get<Output: Decodable>(_ url: String,
output _: Output.Type,
completion: @escaping (Result<Output, NetworkError>) -> Void)
{
AF.request(baseUrl + url).validate().responseDecodable(of: Output.self) { response in
completion(response.result)
}
}
}Without going into too much detail, for now, think of the completion argument like JS callback:
const mainFunction = async (callback: (result: string) => void) => {
const result = 'some data'
// wait for a long running task
await new Promise((resolve) => setTimeout(resolve, 3000))
callback(result)
}
;(async () => {
await mainFunction((result) => {
console.log(`primary task completed with: ${result}`)
})
})()
// output: primary task completed with: some dataNow we’ll create an extension of our NetworkManager class that conforms to Alamofire’s RequestInterceptor protocol and use UserDefaults to add a JWT to outgoing requests.
This assumes a JWT was stored with UserDefaults after successful authentication. We will also adjust our get method to use the interceptor.
import Alamofire
import Foundation
typealias NetworkError = AFError
class NetworkManager {
static let instance = NetworkManager()
private let baseUrl = "http://localhost:3000"
private let userDefaults = UserDefaults.standard
func get<Output: Decodable>(_ url: String,
output _: Output.Type,
completion: @escaping (Result<Output, NetworkError>) -> Void)
{
AF.request(baseUrl + url, interceptor: self).validate().responseDecodable(of: Output.self) { response in
completion(response.result)
}
}
}
extension NetworkManager: RequestInterceptor {
func adapt(_ urlRequest: URLRequest, for _: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var request = urlRequest
if let token = userDefaults.string(forKey: "SUPER_SECURE_JWT") {
let bearerToken = "Bearer \(token)"
request.setValue(bearerToken, forHTTPHeaderField: "Authorization")
}
completion(.success(request))
}
}Let's also add a RequestStatus enum to help us communicate with the user and update the UI while the request is loading or has failed. Using a single value for network status is easier to manage than separate values for each state; you don’t have to set and reset “loading” constantly.
enum RequestStatus {
case loading, success, failure
}Now we can use our NetworkManager in our views or view models.
import SwiftUI
struct MyView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
NavigationStack {
List {
switch viewModel.requestStatus {
case .loading:
ProgressView().listRowBackground(Color.clear)
case .failure:
Text("Something went wrong")
case .success:
ForEach(viewModel.data, id: \.self) { item in
Text(item)
}
}
}.onAppear {
viewModel.loadItems()
}.navigationTitle("My Items")
}
}
}
extension MyView {
@MainActor class ViewModel: ObservableObject {
private let http = NetworkManager.instance
@Published var requestStatus: RequestStatus = .success
@Published var data: [String] = []
func loadItems() {
requestStatus = .loading
http.get("/items", output: [String].self) { result in
do {
self.data = try result.get()
self.requestStatus = .success
} catch {
self.requestStatus = .failure
}
}
}
}
}Hot Tip. When working with JSON and Decodable, don’t print localized descriptions of errors in catch blocks. Print the whole thing to get better messages.
do {
self.data = try result.get()
} catch {
print(error)
// not
print(error.localizedDescription)
}Finally, we can bring it all together and add a few more functions for POST, PUT, PATCH, and DELETE HTTP methods.
import Alamofire
import Foundation
typealias NetworkError = AFError
enum RequestStatus {
case loading, success, failure
}
class NetworkManager {
static let instance: NetworkManager = .init()
private let baseUrl = "http://localhost:3000" // value from env var or config
private let userDefaults = UserDefaults.standard
func get<Output: Decodable>(_ url: String,
output _: Output.Type,
completion: @escaping (Result<Output, NetworkError>) -> Void)
{
AF.request(baseUrl + url, interceptor: self).validate().responseDecodable(of: Output.self) { response in
completion(response.result)
}
}
func delete<Output: Decodable>(_ url: String,
output _: Output.Type,
completion: @escaping (Result<Output, NetworkError>) -> Void)
{
AF.request(baseUrl + url, method: .delete,
interceptor: self).validate().responseDecodable(of: Output.self) { response in
completion(response.result)
}
}
func post<Input: Encodable, Output: Decodable>(_ url: String,
input: Input,
output _: Output.Type,
completion: @escaping (Result<Output, NetworkError>) -> Void)
{
AF.request(baseUrl + url,
method: .post,
parameters: input,
encoder: .json,
headers: [
.init(name: "Content-Type", value: "application/json"),
],
interceptor: self)
.validate()
.responseDecodable(of: Output.self) { response in
completion(response.result)
}
}
func put<Input: Encodable, Output: Decodable>(_ url: String,
input: Input,
output _: Output.Type,
completion: @escaping (Result<Output, NetworkError>) -> Void)
{
AF.request(baseUrl + url,
method: .put,
parameters: input,
encoder: .json,
headers: [
.init(name: "Content-Type", value: "application/json"),
],
interceptor: self)
.validate()
.responseDecodable(of: Output.self) { response in
completion(response.result)
}
}
func patch<Input: Encodable, Output: Decodable>(_ url: String,
input: Input,
output _: Output.Type,
completion: @escaping (Result<Output, NetworkError>) -> Void)
{
AF.request(baseUrl + url,
method: .patch,
parameters: input,
encoder: .json,
headers: [
.init(name: "Content-Type", value: "application/json"),
],
interceptor: self)
.validate()
.responseDecodable(of: Output.self) { response in
completion(response.result)
}
}
}
extension NetworkManager: RequestInterceptor {
func adapt(_ urlRequest: URLRequest, for _: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var request = urlRequest
if let token = userDefaults.string(forKey: "SUPER_SECURE_JWT") {
let bearerToken = "Bearer \(token)"
request.setValue(bearerToken, forHTTPHeaderField: "Authorization")
}
completion(.success(request))
}
}Async/Await
With Alamofire you can also do this using async/await and avoid the callback Pyramid of Doom. Here is an example of what the get method on would look like:
func get<Output: Decodable>(_ url: String,
output _: Output.Type) async -> Result<Output, NetworkError>
{
let task = AF.request(baseUrl + url, interceptor: self).validate().serializingDecodable(Output.self)
return await task.result
}I hope these snippets help you with your next Swift/SwiftUI (or TS) project!