Supporting Low Data Mode in your app

Published on: September 23, 2019

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

  1. By configuring your URLRequest
  2. By applying a configuration to a URLSession
  3. 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!

Categories

Combine

Subscribe to my newsletter