May 24, 2018
We commonly say that you should only use libraries that you thoroughly understand but we rarely take the time to really dig into those libraries to see how they work. How exactly does Alamofire use a URLRequest
or a URL String to make a network call?
This tutorial uses Swift 4 and Alamofire 4.7.
The syntax to make a networking request makes it a little difficult to guess what’s happening within Alamofire. If we’re providing a URL string it looks like this:
Alamofire.request(myURLString)
.responseJSON { response in
// do stuff with the JSON or error
}
Or we could provide a URLRequest
:
let url = URL(string: myURLString)!
let urlRequest = URLRequest(url: url)
Alamofire.request(urlRequest)
.responseJSON { response in
// do stuff with the JSON or error
}
The same thing would look like this if we were using URLSession
directly:
let url = URL(string: myURLString)!
let urlRequest = URLRequest(url: url)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// serialize JSON
// do stuff with the JSON or error
}
task.resume()
Once we’ve set up the URLRequest
and the URLSession
, we’re creating a dataTask
with it then using resume()
to send it. The completion handler for the dataTask
lets us work with the results of the call. That’s where we could use JSONSerialization
to convert the results to JSON or handle any errors.
Since Alamofire is a wrapper around URLSession
there should be code in Alamofire there that creates a dataTask
then sends it using .resume()
. So let’s look at the Alamofire code to see if we can figure out how that actually happens.
What does Alamofire.request(…) do?
Alamofire.request(myURLString)
is a function call. To see the code for that function, mouse over it in Xcode then cmd-click on it or right-click and select “Jump to Definition”.
The definition is in Alamofire.swift
and it looks like this for the URLRequest
version of Alamofire.request
:
public func request(_ urlRequest: URLRequestConvertible) -> DataRequest {
return SessionManager.default.request(urlRequest)
}
There’s a similar version for the URL String version of Alamofire.request
.
@discardableResult
public func request(
_ url: URLConvertible,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.default,
headers: HTTPHeaders? = nil)
-> DataRequest {
return SessionManager.default.request(
url,
method: method,
parameters: parameters,
encoding: encoding,
headers: headers
)
}
Which shows us all of the optional arguments: method
, parameters
, encoding
, and headers
. We can use those to make other types of HTTP requests.
The underlying code is basically the same for both versions of Alamofire.request
so we’ll focus on the URL String version.
All that this fuction does is call a similar function on the default SessionManager
with all of the optional arguments set. To see it right-click and select “Jump to Definition” (or cmd-click) again:
@discardableResult
open func request(
_ url: URLConvertible,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.default,
headers: HTTPHeaders? = nil)
-> DataRequest {
var originalRequest: URLRequest?
do {
originalRequest = try URLRequest(url: url, method: method, headers: headers)
let encodedURLRequest = try encoding.encode(originalRequest!, with: parameters)
return request(encodedURLRequest)
} catch {
return request(originalRequest, failedWith: error)
}
}
Protip: request(...)
returns a DataRequest
. That’s an Alamofire class that inherits from Alamofire.Request
. Alamofire.Request
has a very handy feature: the debugDescription
returns a cURL statement that’s equivalent to the Alamofire request. So if you’re having trouble debugging an API call in your app, use let request = Alamofire.request(...)
then debugPrint(request)
after the completion handler(s). Then you can compare the cURL statement to your API docs, share it with a backend dev to see what’s wrong, or paste it into Terminal so you can tweak it there to figure out what you should be sending.
What’s the Session Manager
The SessionManager
is what really does the work in Alamofire. Calls like Alamofire.request(...)
are just convenient short-hand for similar calls to the default SessionManager
like SessionManager.default.request(...)
. This code:
Alamofire.request(myURLString)
.responseJSON { response in
// do stuff with the JSON or error
}
Does the same thing as this:
let sessionManager = Alamofire.SessionManager.default
sessionManager.request(myURLString)
.responseJSON { response in
// do stuff with the JSON or error
}
You can create a non-default SessionManager
if you want to use URLSessionConfiguration
to set up your session. For example, you can use it to create a background session or to set default headers that should be included with all network calls in the session.
For more details, see the SessionManager docs.
Now, back to digging into the Alamofire code to figure out what’s happening when we call Alamofire.request(...)
.
What does SessionManager.default.request(…) do?
Here’s the request
function that we’ve dug down to:
@discardableResult
open func request(
_ url: URLConvertible,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.default,
headers: HTTPHeaders? = nil)
-> DataRequest {
var originalRequest: URLRequest?
do {
originalRequest = try URLRequest(url: url, method: method, headers: headers)
let encodedURLRequest = try encoding.encode(originalRequest!, with: parameters)
return request(encodedURLRequest)
} catch {
return request(originalRequest, failedWith: error)
}
}
request(...)
in SessionManager
creates a URLRequest
with all of the inputs you provided, including encoding parameters. Then it just returns it. That doesn’t seem to do much…
But notice that it creates a DataRequest
like return request(encodedURLRequest)
or return request(originalRequest, failedWith: error)
. Cmd-click to see what request
does there.
@discardableResult
open func request(_ urlRequest: URLRequestConvertible) -> DataRequest {
var originalRequest: URLRequest?
do {
originalRequest = try urlRequest.asURLRequest()
let originalTask = DataRequest.Requestable(urlRequest: originalRequest!)
let task = try originalTask.task(session: session, adapter: adapter, queue: queue)
let request = DataRequest(session: session, requestTask: .data(originalTask, task))
delegate[task] = request
if startRequestsImmediately { request.resume() }
return request
} catch {
return request(originalRequest, failedWith: error)
}
}
If you dug down through the URL string version of Alamofire.request
then you’d get to this point too.
This function takes a URLRequest
(or at least something that can easily be converted to one, see the discussion of URLRequestConvertible in this post for details).
urlRequest.asURLRequest()
converts whatever was passed in into a URLRequest
. E.g., if you passed a URL string like https://grokswift.com
you’d end up with a URLRequest
to make a GET
request to that URL
with no parameters, no non-default headers, and no encoding.
Then it creates a DataRequest.Requestable(...)
and calls originalTask.task(...)
on it. Let’s cmd-click on Requestable
to see what those two calls do before continuing on with the function we’ve been looking at:
struct Requestable: TaskConvertible {
let urlRequest: URLRequest
func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
do {
let urlRequest = try self.urlRequest.adapt(using: adapter)
return queue.sync { session.dataTask(with: urlRequest) }
} catch {
throw AdaptError(error: error)
}
}
}
There’s nothing special in the DataRequest.Requestable(...)
initializer, it’s just the default member-wise struct initializer so it just sets the value of urlRequest
.
func task(...)
looks like we’re getting closer to where the magic happens.
self.urlRequest.adapt(using: adapter)
is neat it’s but not what we’re focused on right now. RequestAdapter
lets you tweak URLRequests
before they get sent.
session.dataTask(with: urlRequest)
is exactly the code we’ve been looking for. It’s creating a dataTask
with a URLSession
. But it’s not immediately sending it since there’s no call to .resume()
. Instead it’s being done within queue.sync { ... }
.
The queue is being passed in when task(...)
is called:
let task = try originalTask.task(session: session, adapter: adapter, queue: queue)
It’s part of SessionManager
and is declared as:
let queue = DispatchQueue(label: "org.alamofire.session-manager." + UUID().uuidString)
It’s a shared queue for the Alamofire session (unless you’ve passed in a custom one). queue.sync { ... }
executes the contents of the code block (the stuff between {
and }
) and waits for it to finish. It prevents multiple calls like that happening at the same time.
At this point we’ve found where the dataTask
is created but not where it’s sent using resume()
.
Back up to SessionManager.default.request(…)
Now that we know what happens when DataRequest.Requestable
is called, let’s figure out the rest of SessionManager.default.request(...)
:
open func request(_ urlRequest: URLRequestConvertible) -> DataRequest {
var originalRequest: URLRequest?
do {
originalRequest = try urlRequest.asURLRequest()
let originalTask = DataRequest.Requestable(urlRequest: originalRequest!)
let task = try originalTask.task(session: session, adapter: adapter, queue: queue)
let request = DataRequest(session: session, requestTask: .data(originalTask, task))
delegate[task] = request
if startRequestsImmediately {
request.resume()
}
return request
} catch {
return request(originalRequest, failedWith: error)
}
}
Before running the task, it gets packed up in an Alamofire.Request
:
let request = DataRequest(session: session, requestTask: .data(originalTask, task))
And stored it by giving it to the delegate:
delegate[task] = request
By default, startsRequestImmediately
is true:
open var startRequestsImmediately: Bool = true
So request.resume()
gets called. That looks like the other half of the URLSession
code that we’re looking for but request
is an Alamofire DataRequest
, not a URLRequest
. To find where the dataTask
gets sent using resume()
, we need to look at the definition of DataRequest.resume()
:
open func resume() {
guard let task = task else {
delegate.queue.isSuspended = false
return
}
if startTime == nil {
startTime = CFAbsoluteTimeGetCurrent()
}
task.resume()
NotificationCenter.default.post(
name: Notification.Name.Task.DidResume,
object: self,
userInfo: [Notification.Key.Task: task]
)
}
After checking that it has a task
, the startTime
gets recorded and task.resume()
is called.
task
in this case is a property of Request
:
open var task: URLSessionTask? { return delegate.task }
It gets the task
from the delegate, where we just stored it.
So that task.resume()
is the other half of that URLSession
code that we’ve been looking for!
To summarize, the URLSession.dataTask
is created by the SessionManager
like:
originalTask.task(session: session, adapter: adapter, queue: queue)
Then sent by:
if startRequestsImmediately {
request.resume()
}
Which calls task.resume()
in Alamofire.Request
.
As long as the queue isn’t suspended and startRequestsImmediately
is true.
Finally, a notification gets posted to let anyone who is interested know that this task has been resumed. (Remember, .resume()
can start a dataTask
as well as resuming one that’s been paused.)
So we’ve figured out how calling Alamofire.request
ends up making a networking request using URLSession.dataTask
.
What if startRequestsImmediately is false
If startRequestsImmediately
isn’t true then the SessionManager
won’t fire off the request. But Alamofire.request(...)
returns the DataRequest
so you can start the request like this:
let request = Alamofire.request(myURLString)
.responseJSON { response in
// do stuff with the JSON or error
}
request.resume()
What does the delegate do?
We didn’t really look at this line in the SessionManager.default.request(...)
function:
delegate[task] = request
delegate
is a SessionDelegate
(again, cmd-click to see where it’s defined):
open let delegate: SessionDelegate
According to the docs:
By default, an Alamofire
SessionManager
instance creates aSessionDelegate
object to handle all the various types of delegate callbacks that are generated by the underlyingURLSession
.
The SessionDelegate
lets you get more control over what happens when sending network requests. It has a few closures that you can override to provide custom handling for things like authentication challenges, background sessions finishing all their events, HTTP redirection, caching results from a networking call, …
TL; DR
In short, here’s what we figured out:
Alamofire.request(...)
gets called- It asks the
SessionManager
to actually do it SessionManager
creates aURLRequest
- A
DataRequest
gets created - You get a chance to change the
URLRequest
usingRequestAdapter
- The
DataRequest
creates adataTask
- The
SessionDelegate
keeps track of theDataRequest
anddataTask
- If
startRequestsImmediately
is true, theSessionManager
starts thedataTask
- The
DataRequest
is returned to the caller, so they can start it ifstartRequestsImmediately
is false (or pause it or cancel it or whatever)
And that’s how Alamofire sends networking requests.
And that’s how it works!
We’ve figured out how Alamofire makes network calls using the URLSession
functions. Next time we’ll look at how we actually get the data in the response in the response handlers.
If you’d like more Swift tutorials on topics like this one, sign up below to get them sent directly to your inbox.