Alamofire Tutorial for iOS: Advanced Usage

In this tutorial, you’ll learn about the advanced usage of Alamofire. Topics include handling OAuth, network logging, reachability, caching and more. By Vidhur Voora.

5 (23) · 3 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Integrating RequestInterceptor

In Networking, create a new Swift file named GitRequestInterceptor.swift. Open the file and add:

import Alamofire

class GitRequestInterceptor: RequestInterceptor {
  //1
  let retryLimit = 5
  let retryDelay: TimeInterval = 10
  //2
  func adapt(
    _ urlRequest: URLRequest,
    for session: Session,
    completion: @escaping (Result<URLRequest, Error>) -> Void
  ) {
    var urlRequest = urlRequest
    if let token = TokenManager.shared.fetchAccessToken() {
      urlRequest.setValue("token \(token)", forHTTPHeaderField: "Authorization")
    }
    completion(.success(urlRequest))
  }
  //3
  func retry(
    _ request: Request,
    for session: Session,
    dueTo error: Error,
    completion: @escaping (RetryResult) -> Void
  ) {
    let response = request.task?.response as? HTTPURLResponse
    //Retry for 5xx status codes
    if 
      let statusCode = response?.statusCode,
      (500...599).contains(statusCode),
      request.retryCount < retryLimit {
        completion(.retryWithDelay(retryDelay))
    } else {
      return completion(.doNotRetry)
    }
  }
}

Here's a step-by-step breakdown:

The method inspects and adapts the request. Because the completion handler is asynchronous, this method can fetch a token from the network or disk before making the request.

Here, you fetch the token from the keychain and add it to an Authorization header. The access token for GitHub OAuth Apps doesn't have an expiration time. However, the user who authorized the app can revoke it through GitHub settings.

Here, you check if the response code contains a 5xx error code. The server returns 5xx codes when it fails to fulfill a valid request. For example, you could get a 503 error code when the service is down for maintenance.

If the error contains a 5xx error code the request is retried with the delay specified in retryDelay, provided the count is within retryLimit.

  1. You declare two constants, retryLimit and retryDelay. They help enforce limits on the number of attempts for retrying a request and the duration between retry attempts.
  2. RequestAdapter is part of RequestInterceptor. It has a single requirement, adapt(_:for:completion:).

    The method inspects and adapts the request. Because the completion handler is asynchronous, this method can fetch a token from the network or disk before making the request.

    Here, you fetch the token from the keychain and add it to an Authorization header. The access token for GitHub OAuth Apps doesn't have an expiration time. However, the user who authorized the app can revoke it through GitHub settings.

  3. RequestRetrier has a single requirement, retry(_:for:dueTo:completion:). The method is called when a request encounters an error. You have to call the completion block with RetryResult to indicate whether the request should be retried.

    Here, you check if the response code contains a 5xx error code. The server returns 5xx codes when it fails to fulfill a valid request. For example, you could get a 503 error code when the service is down for maintenance.

    If the error contains a 5xx error code the request is retried with the delay specified in retryDelay, provided the count is within retryLimit.

Open GitAPIManager.swift. Add the following code below let networkLogger = GitNetworkLogger() in sessionManager:

let interceptor = GitRequestInterceptor()

Here, you define interceptor as an instance of GitRequestInterceptor. Replace Session initialization with the following in sessionManager:

return Session(
  configuration: configuration,
  interceptor: interceptor,
  eventMonitors: [networkLogger])

With this code you pass the newly created interceptor in the constructor of Session. All requests belonging to sessionManager are now intercepted via the instance of GitRequestInterceptor. Build and run.

Logged in user repositories

Voilà! Now you'll see repositories fetched from your GitHub account.

Routing Requests and URLRequestConvertible

So far, when making a network request, you've provided the URL path, HTTP method and query parameters for each request. As the app size grows, it's essential to use some common patterns for building the network stack. A Router design pattern helps by defining each request's route and components.

In Networking, open GitRouter.swift. You'll see all the requests you've made so far, captured as different cases in the enum. Use this GitRouter to construct your requests.

Add the following extension to the end of GitRouter.swift:

//1
extension GitRouter: URLRequestConvertible {
  func asURLRequest() throws -> URLRequest {
    //2
    let url = try baseURL.asURL().appendingPathComponent(path)
    var request = URLRequest(url: url)
    request.method = method
    //3
    if method == .get {
      request = try URLEncodedFormParameterEncoder()
        .encode(parameters, into: request)
    } else if method == .post {
      request = try JSONParameterEncoder().encode(parameters, into: request)
      request.setValue("application/json", forHTTPHeaderField: "Accept")
    }
    return request
  }
}

Here's a breakdown:

  1. You add an extension to GitRouter to conform to URLRequestConvertible. The protocol has a single requirement, asURLRequest(), which helps construct a URLRequest. Conforming to URLRequestConvertible helps abstract and ensure the consistency of requested endpoints.
  2. Here, you construct the request using the properties in GitRouter.
  3. Based on the HTTP method, you encode the parameters using either URLEncodedFormParameterEncoder or JSONParameterEncoder. The Accept HTTP header is set for POST request. You return the request after constructing it.

Now, open GitAPIManager.swift. You'll update all the request methods to use GitRouter.

In fetchCommits(for:completion:), delete the line that begins let url =. Now, replace sessionManager.request(url) with the following to use your new router:

sessionManager.request(GitRouter.fetchCommits(repository))

In searchRepositories(query:completion:), delete everything before sessionManager.request.... Now, replace sessionManager.request(url, parameters: queryParameters) with:

sessionManager.request(GitRouter.searchRepositories(query))

Likewise, in fetchAccessToken(accessCode:completion:), delete everything before sessionManager.request.... Now, replace the sessionManager.request(...) call with:

sessionManager.request(GitRouter.fetchAccessToken(accessCode))

Finally, in fetchUserRepositories(completion:), delete everything before sessionManager.request(url, parameters: parameters) and replace that line with:

sessionManager.request(GitRouter.fetchUserRepositories)

You remove the URL, query parameters and headers declared locally in each of these methods since you no longer need them. GitRouter constructs URLRequests for each request. Build and run.

Logged in user repositories

You'll see your repositories load as before except the underlying requests use GitRouter.

So far, the app works well. With a good network, the results are almost instantaneous. But the network is one unpredictable beast.

It's important to know when the network isn't reachable and inform the user in your app. Alamofire's NetworkReachabilityManager at your service!

Network Reachability

In the Networking group, open GitNetworkReachability.swift. Add the following just before the closing brace:

// 1
let reachabilityManager = NetworkReachabilityManager(host: "www.google.com")
// 2
func startNetworkMonitoring() {
  reachabilityManager?.startListening { status in
    switch status {
    case .notReachable:
      self.showOfflineAlert()
    case .reachable(.cellular):
      self.dismissOfflineAlert()
    case .reachable(.ethernetOrWiFi):
      self.dismissOfflineAlert()
    case .unknown:
      print("Unknown network state")
    }
  }
}

GitNetworkReachability provides a shared instance. It includes the functionality to show and dismiss an alert. Here's what you added:

  1. Alamofire's NetworkReachabilityManager listens for the reachability of hosts and addresses. It works on both cellular and WiFi network interfaces. Here, you create a property, reachabilityManager, as an instance of NetworkReachabilityManager. This checks for reachability using www.google.com as the host.
  2. startNetworkMonitoring() listens for changes in the network reachability status. If the network isn't reachable, an alert is displayed. Once there's a network that's reachable through any of the network interfaces, the alert is dismissed.

Now, open AppDelegate.swift. Add the following in application(_:didFinishLaunchingWithOptions:) right before return true:

GitNetworkReachability.shared.startNetworkMonitoring()

Here, you call startNetworkMonitoring() on GitNetworkReachability to start listening for network reachability status when the app launches.

Build and run. Once the app launches, turn the network off.

Network reachability on Alamofire

The app shows the user an alert when the network isn't reachable and dismisses it when it's reachable. Great job being user-centric!

Note: You should test functionality related to network reachability on a real device because reachability might not work as expected on a simulator. You can learn more about the issue by reading this GitHub post.

Sometimes showing an alert isn't the ideal experience. Instead, you might prefer showing previously fetched app data when there's no network. Alamofire's ResponseCacher is here to help. :]