Skip to content
On this page

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

swift
// 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

swift
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:

ts
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.

swift
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.

swift
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.

swift
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:

ts
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.

swift
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.

swift
enum RequestStatus {
    case loading, success, failure
}

Now we can use our NetworkManager in our views or view models.

swift
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.

swift
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.

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

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:

swift
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!