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 http
Swift
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 data
Now 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!