Supporting Low Data Mode in your app
Published on: September 23, 2019Together with iOS 13, Apple announced a new feature called Low Data Mode. This feature allows users to limit the amount of data that’s used by apps on their phone. The low data mode setting is available in the settings app. Whenever a user is on a slow network, a very busy network or on a network that might charge them for every megabyte they download, users might not want to spend their limited data budget on large fancy images or clever prefetching logic. With Low Data Mode, users can now inform your app that they are on such a network so you can accommodate their needs accordingly.
It’s up to app developers to support low data mode and handle the user’s preferences with care. In their 2019 WWDC talk Advanced In Networking Part One, Apple suggests to at least adopt low data mode for features that will have limited to no impact on the user experience. A good example of this might be to limit the amount of data prefetching and syncing your app does to prevent making requests of which the result will never be visible to the user.
One clever integration example of low data mode that Apple gives in their talk is graceful degradation of images. Any time your app wants to load an image, it should fall back to a smaller, lower-quality version if low data mode is enabled. In this post, I will show you how you can implement a feature like this, and what other ways there are for you to integrate low data mode in your apps.
A simple fallback integration
In its most basic form, you can configure low data mode support separately for each request your app makes. The following code shows how you can create a request that will honor a user’s low data mode preferences:
guard let url = URL(string: "https://someresource.com")
else { return }
var request = URLRequest(url: url)
request.allowsConstrainedNetworkAccess = false
By setting allowsConstrainedNetworkAccess
to false
on the URLRequest
, you’ve done all the work needed to support low data mode. When you attempt to execute this URLRequest
while Low Data Mode is active, it will fail with a URLError
that has its networkUnavailableReason
property set to .constrained
. So whenever your request fails with that error, you might want to request a resource that consumes less data if needed, like a lower quality image. The following code snippet shows how to do this using the request
from the previous snippet:
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
if let networkError = error as? URLError, networkError.networkUnavailableReason == .constrained {
// make a new request for a smaller image
}
// The request failed for some other reason
return
}
if let data = data, let image = UIImage(data: data) {
// image loaded succesfully
return
}
// error: couldn't convert the data to an image
}
I have omitted the error handling and the URLRequest
for the smaller image because these features might be specific for your app and implementing them shouldn’t be too daunting.
In case you’re wondering how you can do this using Apple’s new Combine framework, I’ll gladly show you. The following snippet is a complete example of a method that accepts a high quality and low-quality image URL, attempts to load to high-quality one but falls back on the low-quality version if low data mode is enabled:
func fetchImage(largeUrl: URL, smallUrl: URL) -> AnyPublisher<UIImage, Error> {
var request = URLRequest(url: largeUrl)
request.allowsConstrainedNetworkAccess = false
return URLSession.shared.dataTaskPublisher(for: request)
.tryCatch { error -> URLSession.DataTaskPublisher in
guard error.networkUnavailableReason == .constrained else {
throw error
}
return URLSession.shared.dataTaskPublisher(for: smallUrl)
}.tryMap { (data, _) -> UIImage in
guard let image = UIImage(data: data) else {
throw NetworkError.invalidData
}
return image
}.eraseToAnyPublisher()
}
The above snippet uses the dataTaskPublisher(for:)
method to create a DataTaskPublisher
. If this publisher emits an error, the error is caught to see if the error was thrown due to Low Data Mode being enabled. If this is the case, a new DataTaskPublisher
is created. This time for the low-quality URL. If the request succeeds, the retrieved data is converted to an image in a tryMap
block. If the conversion fails an error is thrown. Note that NetworkError.invalidData
is not a built-in error, it’s one that you’d have to define yourself. Lastly, the result of tryMap
is converted to an AnyPublisher
to make it easier to work with for callers of fetchImage(largeUrl:, smallUrl:)
.
And that’s it! A complete implementation of low data mode with a fallback in less than 20 lines of code.
Enabling low data mode for an entire URLSession
If you find that supporting low data mode on a case by case basis for your requests is tedious, you can also set up an entire URLSession
that restricts network access when low data mode is enabled. I won’t show you how to make requests and implement fallback logic since this is all done identically to the previous example. The only difference is that you should use your own URLSession
instance instead of the URLSession.shared
instance when you create data tasks. Here’s an example of a URLSession
with a configuration that supports low data mode:
var configuration = URLSessionConfiguration.default
configuration.allowsConstrainedNetworkAccess = false
let session = URLSession(configuration: configuration)
With just three lines of code, all of your URL requests (that are made using the custom session) support low data mode! No extra work needed. Pretty rad, right? But what if you want to restrict something else, like for example media playback? No worries, that also works. Let me show you how.
Low data mode for media playback
Media playback is possibly one of the best ways to use heaps of a user’s data in your app. Some apps use video assets as beautiful backgrounds or rely on video for other non-essential tasks. If this sounds like your app, you might want to implement low data mode for media playback. If you’re using AVURLAsset
in your app, implementing low data mode is fairly straightforward. All you need to do is set the AVURLAssetAllowsConstrainedNetworkAccessKey
on the asset’s options dictionary to false
and AVPlayer
takes care of the rest. A small snippet for reference:
guard let mediaUrl = URL(string: "https://yourdomain.com/media.mp4")
else { return }
let asset = AVURLAsset(url: mediaUrl, options: [AVURLAssetAllowsConstrainedNetworkAccessKey: false])
Straightforward and effective, I like it.
Detecting Low Data mode without relying on an error
If you want to be pro-active with your low data mode implementation, and for instance warn a user that they are about to engage in an activity that will potentially use more data than they would like, you can use the Network
framework. Apple's Network
framework is typically used if you want to perform very low-level networking management that you can't achieve with URLSession
. Since URLSession
doesn't appear to have any way to detect whether a user has low data mode turned on before making a request, you must fall back to Network
to do this kind of detection. The following code is an example of how you can detect that a user has low data mode turned on using the Network
framework:
let monitor = NWPathMonitor()
monitor.pathUpdateHandler = { path in
let constrained = path.isConstrained
}
monitor.start(queue: DispatchQueue.global())
The code above uses an NWPathMonitor
to monitor the available networking paths on the device. When a path becomes available or if a path changes, the monitor will call its pathUpdateHandler
with the available, recently changed path. The monitor won't work until you call its start
method. Depending on your needs, coding styles and conventions you might want to create a dedicated dispatch queue that the monitor will call the update handler on. Also note that you'll want to keep a reference to the NWPathMonitor
in the object that is using the monitor to prevent it from being deallocated before its update handler is called.
Thanks to @codeOfRobin for pointing out that NWPath
can help with detecting low data mode.
Other considerations to limit unwanted data usage
In addition to giving you the ability to support low data mode, Apple has introduced another way for your app to prevent overuse of data. If you want to limit data usage on cellular networks, mobile hotspots and potentially on other expensive networks, you can use the allowsExpensiveNetworkAccess
setting on URLSession
and URLRequest
or the AVURLAssetAllowsExpensiveNetworkAccessKey
key on AVURLAsset
to limit expensive data usage. NWPath
also has an isExpensive
property to detect whether a user is using an expensive network. If you’re currently restricting cellular access, you might want to consider checking for expensive networks instead. After all, cellular networks are improving all the time and maybe someday in the future they might not always be considered expensive anymore.
In conclusion
This post has shown you several ways to support low data mode in your app:
- By configuring your
URLRequest
- By applying a configuration to a
URLSession
- By configuring your
AVURLAsset
for low data mode
You also learned how to implement an image loader that falls back to a lower quality image using Combine, which is pretty cool on its own!
It is now up to you to come up with clever fallbacks, and to take a good look at the data you send and receive over the network, and ask yourself whether you truly need to make every request, or maybe you can optimize things a little bit.
As always, feedback, compliments, and questions are welcome. You can find me on Twitter if you want to reach out to me.
Also, thanks to @drarok for helping me make the Swift compiler happy with the Combine part of this post!