Reduce your iOS apps network traffic with ETags using URLSession or Moya
Disclaimer: I'm an iOS engineer and no expert for networking or server side configurations. Everything in this article describes how I understand this topic. If you find anything wrong please give me a hint.
Introduction
Many apps rely on network requests to work. Depending on your use case the amount and frequency of those requests may vary. To be a good iOS citizen you should try to reduce your apps network traffic. This saves battery and data volume and can improve app performance by avoiding the need to wait for network requests to finish. But how does the app know when to ask for new data and when not? Luckily there are mechanisms available in the http protocol: ETag and Caching.
What is an ETag?
An ETag is a string in the response header identifying one resource. If on a new request to this same endpoint anything in this response has changed, then the ETag will be different. You can see it as a fingerprint of one response. This feature enables us to know when something in the dataset has changed and needs to get updated in the app.
ETag: "2b837255a2cb690c42da56336b51c894"
How can we use ETag to reduce network traffic
When requesting a data from an endpoint, the server includes the ETag header field in the response. For the next Request to this endpoint, we can send the Etag from the last response in the If-None-Match
header field. With this information the server can check if the data it would return has changed since the last time. If there are no changes, the server returns no data and the status code 304
(Not Modified) instead of 200
(Ok). Otherwise it will return the updated response with status code 200 and a new ETag identifying this responses data.
How ETag and Cache work together
Getting a 304
response is a good thing. It means there is no need to transfer unnecessary data over network. But how should we handle this response when there is no data? Usually we use a completion handler to parse the response data and return it as model. As there is no data to parse now this will result in an error. One way would be to store old responses manually and retrieve them when a 304
arrives.
A better way would be to use a build in mechanism for that: URLCache. By default a URLSession
caches a response when its header defines the caching behavior. If there is no cache header specified or it denies caching, then URLSession will not cache the response. When there is a cached response, URLSession
will return that cached response once it receives a 304
with a matching ETag. This means our completion handler receives a usual response with status code 200
. Another advantage is that as long as the cached response is valid, URLSession
won't send any requests but return the cached response immediately.
Configure the web server
To allow our app to efficiently work with ETags and Caching, the web server needs to support that. Responses must allow caching and have an ETag. Both is set in the header fields:
Cache-Control: public, max-age=36000
ETag: "2b837255a2cb690c42da56336b51c894"
Additionally the server needs to know how to handle a If-None-Match
header, when our app sends it.
If-None-Match: "2b837255a2cb690c42da56336b51c894"
Configure the app using bare URLSession
When using URLSession
directly you can use the URLSession.shared
as it has URLCache.shared
as default cache.
let session = URLSession.shared // uses URLCache.shared by default
var request = URLRequest(url: aURL)
// set to use the cache behavior defined by the server
request.cachePolicy = .useProtocolCachePolicy
let task = session.dataTask(with: request) { data, response, error in
if let httpResponse = response as? HTTPURLResponse,
let etag = httpResponse.allHeaderFields["Etag"] as? String {
// store ETag
}
}
task.resume()
And don't forget to set the cachePolicy
for you requests. .useProtocolCachePolicy
should be the default, but it wouldn't hurt to set this explicit.
To get the advantage of the stored ETag add it to your next request.
request.addValue(storedEtag, forHTTPHeaderField: "Etag")
Configure the app using Moya
In Moya you first have to define a protocol to make your TargetType
cacheable.
protocol CachePolicyGettable {
var cachePolicy: URLRequest.CachePolicy? { get }
}
Then extend your TargetType
with this new protocol.
extension YourTargetType: CachePolicyGettable {
var cachePolicy: URLRequest.CachePolicy {
.useProtocolCachePolicy
}
}
To enable the Moya provider to use this protocol to add the cachePolicy
to the request, you need a custom Moya plugin.
final class CachePolicyPlugin: PluginType {
func prepare(_ request: URLRequest, target: YourTargetType) -> URLRequest {
guard let policyGettable = target as? CachePolicyGettable else {
return request
}
var mutableRequest = request
mutableRequest.cachePolicy = policyGettable.cachePolicy
return mutableRequest
}
}
Now the last step is to tell Maya about your plugin. This is done when initializing your MoyaProvider
.
let provider = MoyaProvider<YourTargetType>(plugins: [CachePolicyPlugin()])
To set the If-None-Match
header, a TargetType
has a headers
property that can help here.
var headers: [String: String]? {
return ["Etag": storedEtag]
}
Conclusion
Caching responses helps your app to reduce network traffic and therefore increase performance. To take advantage of the built in caching behavior in iOS your app and the server need to support this behavior actively.